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

export deka,hecto,kilo,mega,giga,tera,peta,exa,zetta,yotta
export deci,centi,milli,micro,nano,pico,femto,atto,zepto,yocto
export byte,kibi,mebi,gibi,tebi,pebi,exbi,zebi,yobi

export slug, ft, KJ1990, KJ2014, RK1990, RK2014, mₑ1990, mₑ2014, temp, units, °R, T₀, eV
export slugs, kilograms, lbm, meters, feet, rankine, kelvin, moles, molecules, universal
export Universe, UnitSystem, US, universe, HOUR, DAY, th, lc, mc, tcq, lcq, mcq
export similitude, 𝟙, F, M, L, T, Q, Θ, N, J, A, Λ, C, sackurtetrode
export °R, τ, 𝟏𝟎, 𝟐, 𝟑, 𝟓, nm, 𝟏, mₑ, μ₀, Mᵤ, Rᵤ, αG, GG, slug, ħ, μₚₑ, αL, 𝟕, 𝟏𝟏, 𝟏𝟗, 𝟒𝟑

const EMU2019,ESU2019,stiffness = EMU,ESU,fluence

# == Metric is different
const eV = electronvolt(SI2019)
const κ = einstein(SI2019)
const σ = stefan(SI2019) #
const μB = magneton(SI2019) #
const ε₀ = vacuumpermittivity(SI2019) #
const kₑ = electrostatic(SI2019) #
const mₚ = protonmass(SI2019)
const mᵤ = atomicmass(SI2019)
const 𝔉 = faraday(SI2019) #
const Φ₀ = magneticfluxquantum(SI2019) #
const Z₀ = vacuumimpedance(SI2019) #
const G₀ = conductancequantum(SI2019) #
const Eₕ = hartree(SI2019)
const a₀ = bohr(SI2019)
const rₑ = electronradius(SI2019)
const RH,Ry = R∞*mₚ/(electronmass(SI2019)+mₚ),𝘩*𝘤*R∞

const ℓP = length(PlanckGauss,SI2019)
const tP = time(PlanckGauss,SI2019)
const TP = temperature(PlanckGauss,SI2019)

const lS = length(Stoney,SI2019)
const tS = time(Stoney,SI2019)
const mS = mass(Stoney,SI2019)
const qS = charge(Stoney,SI2019)

const lA = length(Hartree,SI2019)
const tA = time(Hartree,SI2019)
const mA = mass(Hartree,SI2019)
const qA = charge(Hartree,SI2019)

const lQCD = length(QCD,SI2019)
const tQCD = time(QCD,SI2019)
const mQCD = mass(QCD,SI2019)

# non standard units

const BTU = thermalunit(British)
const BTUftlb = thermalunit(British) # BTU⋅ft⁻¹⋅lb⁻¹
const BTUJ = thermalunit(SI2019) # BTU⋅J⁻¹
const HP = horsepower(Metric)
const gal = gallon(Metric)
const kcal = kilocalorie(SI2019)
const cal = calorie(SI2019)
const universal = molargas

# constant aliases

const US,mpe, meu, mpu, ainv, aG = UnitSystem,μₚₑ, μₑᵤ, μₚᵤ, αinv, αG
const Mu,Ru,SB,hh,cc,m0,e0,ke,me,mp,mu,ee,FF,Z0,G0,Eh,a0,re,g0,lP,aL,ϵ₀ = Mᵤ,Rᵤ,σ,𝘩,𝘤,μ₀,ε₀,kₑ,mₑ,mₚ,mᵤ,𝘦,𝔉,Z₀,G₀,Eₕ,a₀,rₑ,g₀,ℓP,αL,ε₀
export κ, G, GG, NA, kB, Rᵤ, σ, 𝘩, ħ, 𝘤, μ₀, ε₀, kₑ, mₑ, mₚ, mᵤ, 𝘦, 𝔉, Φ₀, Z₀, G₀, Eₕ, R∞, a₀, rₑ, KJ, RK, Ru, SB, hh, cc, m0, e0, ke, me, mp, mu, ee, FF, Z0, G0, Eh, a0, re, μB
export αG, αinv, μₚₑ, μₑᵤ, μₚᵤ, mpe, meu, mpu, mP, δμ₀, Mᵤ, Mu, RH, Ry, ΔνCs, Kcd, ainv
export cal, kcal, calₜₕ, kcalₜₕ, calᵢₜ, kcalᵢₜ, ℓP, g₀, g0, atm, lbm, BTUJ, BTUftlb, aG
export lP, tP, TP, lS, tS, mS, qS, lA, tA, mA, qA, lQCD, tQCD, mQCD, ϵ₀, αL, aL

@doc """
    Constant{D} <: Real

Numerical constant `D` used in `UnitSystem` derivations.
```Julia
julia> Constant(100)
$(Constant(100))
```
Can be multiplied, added, subtracted, and so on.
""" Constant

# engineering unit systems docs

@doc """
    Metric = MetricSystem(milli,𝟐*τ/𝟏𝟎^7)

Standard `Metric` system based on exact `molarmass` and `vacuumpermeability`.

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

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

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

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

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

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

julia> luminousefficacy(Metric) # lm⋅W⁻¹
$(luminousefficacy(Metric))
```
""" Metric, MKS

@doc """
    SI2019 = MetricSystem(Mᵤ,μ₀)

Systeme International d'Unites based on approximate `molarmass` and `vacuumpermeability`.

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

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

julia> lightspeed(SI2019) # m⋅s⁻¹
$(lightspeed(SI2019))

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

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

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

julia> luminousefficacy(SI2019) # lm⋅W⁻¹
$(luminousefficacy(SI2019))
```
""" SI2019, SI

@doc """
    MetricEngineering = MetricSystem(milli,𝟐*τ/𝟏𝟎^7,Rᵤ,g₀)

Standard `MetricEngineering` system based on kilogram and kilopond (kilogram-force) units.

```Julia
julia> boltzmann(MetricEngineering) # kgf⋅m⋅K⁻¹
$(boltzmann(MetricEngineering))

julia> planckreduced(MetricEngineering) # kgf⋅m⋅s⋅rad⁻¹
$(planckreduced(MetricEngineering))

julia> lightspeed(MetricEngineering) # m⋅s⁻¹
$(lightspeed(MetricEngineering))

julia> vacuumpermeability(MetricEngineering) # kgf⋅s²⋅C⁻²
$(vacuumpermeability(MetricEngineering))

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

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

julia> luminousefficacy(MetricEngineering) # lm⋅s⋅m⁻¹⋅kgf⁻¹
$(luminousefficacy(MetricEngineering))

julia> gravity(MetricEngineering) # kg⋅m⋅kgf⁻¹⋅s⁻²
$(gravity(MetricEngineering))
```
""" MetricEngineering, ME

@doc """
    SI2019Engineering = MetricSystem(Mᵤ,μ₀,Rᵤ,g₀)

Systeme International d'Unites based on kilogram and kilopond (kilogram-force) units.

```Julia
julia> boltzmann(SI2019Engineering) # kgf⋅m⋅K⁻¹
$(boltzmann(SI2019Engineering))

julia> planckreduced(SI2019Engineering) # kgf⋅m⋅s⋅rad⁻¹
$(planckreduced(SI2019Engineering))

julia> lightspeed(SI2019Engineering) # m⋅s⁻¹
$(lightspeed(SI2019Engineering))

julia> vacuumpermeability(SI2019Engineering) # kgf⋅s²⋅C⁻²
$(vacuumpermeability(SI2019Engineering))

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

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

julia> luminousefficacy(SI2019Engineering) # lm⋅s⋅m⁻¹⋅kgf⁻¹
$(luminousefficacy(SI2019Engineering))

julia> gravity(SI2019Engineering) # kg⋅m⋅kgf⁻¹⋅s⁻²
$(gravity(SI2019Engineering))
```
""" SI2019Engineering, SIE

@doc """
    SI1976 = MetricSystem(milli,𝟐*τ/𝟏𝟎^7,8.31432)

Reference `UnitSystem` with universal gas constant of `8.31432` from 1976 standard atmosphere.

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

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

julia> lightspeed(SI1976) # m⋅s⁻¹
$(lightspeed(SI1976))

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

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

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

julia> luminousefficacy(SI1976) # lm⋅W⁻¹
$(luminousefficacy(SI1976))
```
""" SI1976

@doc """
    CODATA = ConventionalSystem(RK2014,KJ2014,Rᵤ2014)

Reference `UnitSystem` based on Committee on Data of the International Science Council.

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

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

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

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

julia> lightspeed(CODATA) # m⋅s⁻¹
$(lightspeed(CODATA))

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

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

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

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

@doc """
    Conventional = ConventionalSystem(RK1990,KJ2014)

Conventional electronic `UnitSystem` with 1990 tuned `josephson` and `klitzing` constants.

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

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

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

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

julia> lightspeed(Conventional) # m⋅s⁻¹
$(lightspeed(Conventional))

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

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

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

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

@doc """
    International = ElectricSystem(Metric,$Ωᵢₜ,$Vᵢₜ)

International `UnitSystem` with United States measurements of `Ωᵢₜ` and `Vᵢₜ`.

```Julia
julia> resistance(International,Metric) # Ω⋅Ω⁻¹
$(resistance(International,Metric))

julia> electricpotential(International,Metric) # V⋅V⁻¹
$(electricpotential(International,Metric))

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

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

julia> lightspeed(International) # m⋅s⁻¹
$(lightspeed(International))

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

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

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

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

@doc """
    InternationalMean = ElectricSystem(Metric,1.00049,1.00034)

International `UnitSystem` with mean measurements of `Ωᵢₜ` and `Vᵢₜ`.

```Julia
julia> resistance(InternationalMean,Metric) # Ω⋅Ω⁻¹
$(resistance(InternationalMean,Metric))

julia> electricpotential(InternationalMean,Metric) # V⋅V⁻¹
$(electricpotential(InternationalMean,Metric))

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

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

julia> lightspeed(InternationalMean) # m⋅s⁻¹
$(lightspeed(InternationalMean))

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

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

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

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

@doc """
    Meridian = EntropySystem(Metric,𝟏,em,em^3,𝟏,τ/𝟐^6/𝟓^7,milli)

Modern ideal `Meridian` system defined by France's original `earthmeter` definition.

```Julia
julia> greatcircle(Meridian) # em
$(greatcircle(Meridian))

julia> boltzmann(Meridian) # eJ⋅K⁻¹
$(boltzmann(Meridian))

julia> planckreduced(Meridian) # eJ⋅s⋅rad⁻¹
$(planckreduced(Meridian))

julia> lightspeed(Meridian) # em⋅s⁻¹
$(lightspeed(Meridian))

julia> vacuumpermeability(Meridian) # kegf⋅s²⋅eC⁻²
$(vacuumpermeability(Meridian))

julia> electronmass(Meridian) # keg
$(electronmass(Meridian))

julia> molarmass(Meridian) # keg⋅eg-mol⁻¹
$(molarmass(Meridian))

julia> luminousefficacy(Meridian) # lm⋅W⁻¹
$(luminousefficacy(Meridian))
```
""" Meridian

@doc """
    MeridianEngineering = EntropySystem(MetricEngineering,𝟏,em,em^3,𝟏,τ/𝟐^6/𝟓^7/g₀^2,milli)

Modern ideal engineering `UnitSystem` variant of the original French `Meridian` system.

```Julia
julia> greatcircle(MeridianEngineering) # em
$(greatcircle(MeridianEngineering))

julia> boltzmann(MeridianEngineering) # kegf⋅em⋅K⁻¹
$(boltzmann(MeridianEngineering))

julia> planckreduced(MeridianEngineering) # kegf⋅em⋅s⋅rad⁻¹
$(planckreduced(MeridianEngineering))

julia> lightspeed(MeridianEngineering) # em⋅s⁻¹
$(lightspeed(MeridianEngineering))

julia> vacuumpermeability(MeridianEngineering) # kegf⋅s²⋅eC⁻²
$(vacuumpermeability(MeridianEngineering))

julia> electronmass(MeridianEngineering) # keg
$(electronmass(MeridianEngineering))

julia> molarmass(MeridianEngineering) # keg⋅eg-mol⁻¹
$(molarmass(MeridianEngineering))

julia> luminousefficacy(MeridianEngineering) # lm⋅s⋅m⁻¹⋅kgf⁻¹
$(luminousefficacy(MeridianEngineering))

julia> gravity(MeridianEngineering) # keg⋅em⋅kegf⁻¹⋅s⁻²
$(gravity(MeridianEngineering))
```
""" MeridianEngineering

cgstext(US,AMP,cgs=eval(US)) = """
```Julia
julia> boltzmann($US) # erg⋅K⁻¹
$(boltzmann(cgs))

julia> planckreduced($US) # erg⋅s⋅rad⁻¹
$(planckreduced(cgs))

julia> lightspeed($US) # cm⋅s⁻¹
$(lightspeed(cgs))

julia> vacuumpermeability($US) # statH⋅cm⁻¹
$(vacuumpermeability(cgs))

julia> electronmass($US) # g
$(electronmass(cgs))

julia> molarmass($US) # g⋅mol⁻¹
$(molarmass(cgs))

julia> luminousefficacy($US) # lm⋅s⋅erg⁻¹
$(luminousefficacy(cgs))

julia> rationalization($US)
$(rationalization(cgs))
```
"""

for U ∈ (:CGSm,:CGSe,:EMU,:ESU)
    (EU,AMP) = QuoteNode.(U ∉ (:CGSe,:ESU) ? (:EMU,:Bi) : (:ESU,:statA))
@eval @doc """
    $($(QuoteNode(U))) = GaussSystem(Metric,$($EU≠:EMU ? "(𝟏𝟎*𝘤)^-2" : "𝟏"),𝟐*τ)

Centimetre-gram-second `UnitSystem` variant based on `$($EU)` (non-rationalized).

$(cgstext($(QuoteNode(U)),$AMP))
""" $U

#=U ∉ (:CGSm,:CGSe) && @eval @doc """
    $(Symbol($(QuoteNode(U)),:2019)) = EntropySystem(SI2019,𝟏,0.01,0.001,𝟏,$($EU≠:EMU ? "1e3*μ₀/𝘤^2" : "1e7*μ₀"))

Centimetre-gram-second `UnitSystem` variant of tuned `SI2019` based on `$($EU)` (rationalized).

$(cgstext(Symbol($(QuoteNode(U)),:2019),$AMP))
""" $(Symbol(U,:2019))=#
end

#=@doc """
    Thomson = GaussSystem(Metric,𝟏,𝟐*τ,𝟏/𝟐)

Centimetre-gram-second `UnitSystem` variant `Thomson` (EMU-Lorentz, non-rationalized).

```Julia
julia> boltzmann(Thomson) # erg⋅K⁻¹
$(boltzmann(Thomson))

julia> planckreduced(Thomson) # erg⋅s⋅rad⁻¹
$(planckreduced(Thomson))

julia> lightspeed(Thomson) # cm⋅s⁻¹
$(lightspeed(Thomson))

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

julia> electronmass(Thomson) # g
$(electronmass(Thomson))

julia> molarmass(Thomson) # g⋅mol⁻¹
$(molarmass(Thomson))

julia> luminousefficacy(Thomson) # lm⋅s⋅erg⁻¹
$(luminousefficacy(Thomson))

julia> rationalization(Thomson)
$(rationalization(Thomson))

julia> lorentz(Thomson)
$(lorentz(Thomson))
```
""" Thomson=#

@doc """
    Gauss = GaussSystem(Metric,𝟏,𝟐*τ,𝟏𝟎^-2/𝘤)

Centimetre-gram-second `UnitSystem` variant `CGS` (Gauss-Lorentz, non-rationalized).

```Julia
julia> boltzmann(Gauss) # erg⋅K⁻¹
$(boltzmann(Gauss))

julia> planckreduced(Gauss) # erg⋅s⋅rad⁻¹
$(planckreduced(Gauss))

julia> lightspeed(Gauss) # cm⋅s⁻¹
$(lightspeed(Gauss))

julia> vacuumpermeability(Gauss) # statH⋅cm⁻¹
$(vacuumpermeability(Gauss))

julia> electronmass(Gauss) # g
$(electronmass(Gauss))

julia> molarmass(Gauss) # g⋅mol⁻¹
$(molarmass(Gauss))

julia> luminousefficacy(Gauss) # lm⋅s⋅erg⁻¹
$(luminousefficacy(Gauss))

julia> rationalization(Gauss)
$(rationalization(Gauss))

julia> lorentz(Gauss)
$(lorentz(Gauss))
```
""" Gauss, CGS

@doc """
    LorentzHeaviside = GaussSystem(Metric,𝟏,𝟏,centi/𝘤)

Centimetre-gram-second `UnitSystem` variant `HLU` (Heaviside-Lorentz, rationalized).

```Julia
julia> boltzmann(LorentzHeaviside) # erg⋅K⁻¹
$(boltzmann(LorentzHeaviside))

julia> planckreduced(LorentzHeaviside) # erg⋅s⋅rad⁻¹
$(planckreduced(LorentzHeaviside))

julia> lightspeed(LorentzHeaviside) # cm⋅s⁻¹
$(lightspeed(LorentzHeaviside))

julia> vacuumpermeability(HLU) # hlH⋅cm⁻¹
$(vacuumpermeability(LorentzHeaviside))

julia> electronmass(LorentzHeaviside) # g
$(electronmass(LorentzHeaviside))

julia> molarmass(LorentzHeaviside) # g⋅mol⁻¹
$(molarmass(LorentzHeaviside))

julia> luminousefficacy(LorentzHeaviside) # lm⋅s⋅erg⁻¹
$(luminousefficacy(LorentzHeaviside))

julia> rationalization(LorentzHeaviside)
$(rationalization(LorentzHeaviside))

julia> lorentz(LorentzHeaviside)
$(lorentz(LorentzHeaviside))
```
""" LorentzHeaviside, HLU

@doc """
    Kennelly = GaussSystem(Metric,𝟏𝟎^-7,𝟐*τ,𝟏,𝟏,𝟏)

Kennelly ? variant `UnitSystem` of the standard `Metric` units ???

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

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

julia> lightspeed(Kennelly) # m⋅s⁻¹
$(lightspeed(Kennelly))

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

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

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

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

julia> rationalization(Kennelly)
$(rationalization(Kennelly))
```
""" Kennelly

@doc """
    GravitationalMetric = EntropySystem(Metric,𝟏,𝟏,g₀)

Standard `GravitationalMetric` system based on `hyl` and `kilopond` units.

```Julia
julia> boltzmann(GravitationalMetric) # kgf⋅m⋅K⁻¹
$(boltzmann(GravitationalMetric))

julia> planckreduced(GravitationalMetric) # kgf⋅m⋅s⋅rad⁻¹
$(planckreduced(GravitationalMetric))

julia> lightspeed(GravitationalMetric) # m⋅s⁻¹
$(lightspeed(GravitationalMetric))

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

julia> electronmass(GravitationalMetric) # hyl
$(electronmass(GravitationalMetric))

julia> molarmass(GravitationalMetric) # hyl⋅mol⁻¹
$(molarmass(GravitationalMetric))

julia> luminousefficacy(GravitationalMetric) # lm⋅s⋅m⁻¹⋅kgf⁻¹
$(luminousefficacy(GravitationalMetric))
```
""" GravitationalMetric, GM

@doc """
    GraviationalSI2019 = EntropySystem(SI2019,𝟏,𝟏,g₀)

Gravitational Systeme International d'Unites based on `hyl` and `kilopond` units.

```Julia
julia> boltzmann(GravitationalSI2019) # kgf⋅m⋅K⁻¹
$(boltzmann(GravitationalSI2019))

julia> planckreduced(GravitationalSI2019) # kgf⋅m⋅s⋅rad⁻¹
$(planckreduced(GravitationalSI2019))

julia> lightspeed(GravitationalSI2019) # m⋅s⁻¹
$(lightspeed(GravitationalSI2019))

julia> vacuumpermeability(GravitationalSI2019) # kgf⋅s²⋅C⁻²
$(vacuumpermeability(GravitationalSI2019))

julia> electronmass(GravitationalSI2019) # hyl
$(electronmass(GravitationalSI2019))

julia> molarmass(GravitationalSI2019) # hyl⋅mol⁻¹
$(molarmass(GravitationalSI2019))

julia> luminousefficacy(GravitationalMetric) # lm⋅s⋅m⁻¹⋅kgf⁻¹
$(luminousefficacy(GravitationalMetric))
```
""" GravitationalSI2019, GSI, GSI2019

@doc """
    GravitationalMeridian = EntropySystem(Metric,𝟏,em,g₀*em^3,𝟏,τ/𝟐^6/𝟓^7/g₀,milli)

Gravitational `UnitSystem` variant of the original French `Meridian` unit defintion.

```Julia
julia> greatcircle(GravitationalMeridian) # em
$(greatcircle(GravitationalMeridian))

julia> boltzmann(GravitationalMeridian) # kegf⋅em⋅K⁻¹
$(boltzmann(GravitationalMeridian))

julia> planckreduced(GravitationalMeridian) # kegf⋅em⋅s⋅rad⁻¹
$(planckreduced(GravitationalMeridian))

julia> lightspeed(GravitationalMeridian) # em⋅s⁻¹
$(lightspeed(GravitationalMeridian))

julia> vacuumpermeability(GravitationalMeridian) # kegf⋅s²⋅eC⁻²
$(vacuumpermeability(GravitationalMeridian))

julia> electronmass(GravitationalMeridian) # ehyl
$(electronmass(GravitationalMeridian))

julia> molarmass(GravitationalMeridian) # ehyl⋅eg-mol⁻¹
$(molarmass(GravitationalMeridian))

julia> luminousefficacy(GravitationalMeridian) # lm⋅s⋅em⁻¹⋅kegf⁻¹
$(luminousefficacy(GravitationalMeridian))
```
""" GravitationalMeridian

@doc """
    MTS = EntropySystem(SI2019,𝟏,𝟏,kilo)

Metre-tonne-second `UnitSystem` variant of `Metric` system.

```Julia
julia> boltzmann(MTS) # kJ⋅K⁻¹
$(boltzmann(MTS))

julia> planckreduced(MTS) # kJ⋅s⋅rad⁻¹
$(planckreduced(MTS))

julia> lightspeed(MTS) # m⋅s⁻¹
$(lightspeed(MTS))

julia> vacuumpermeability(MTS) # kH⋅m⁻¹
$(vacuumpermeability(MTS))

julia> electronmass(MTS) # t
$(electronmass(MTS))

julia> molarmass(MTS) # t⋅mol⁻¹
$(molarmass(MTS))

julia> luminousefficacy(MTS) # lm⋅kW⁻¹
$(luminousefficacy(MTS))
```
""" MTS

@doc """
    KKH = EntropySystem(Metric,HOUR,kilo,𝟏)

Kilometer-kilogram-hour `UnitSystem` variant of `Metric` system.

```Julia
julia> boltzmann(KKH) # kg⋅km²⋅h⁻²⋅K⁻¹
$(boltzmann(KKH))

julia> planckreduced(KKH) # kg⋅km²⋅h⁻¹
$(planckreduced(KKH))

julia> lightspeed(KKH) # km⋅hr⁻¹
$(lightspeed(KKH))

julia> vacuumpermeability(KKH) # kg⋅km⋅C⁻²
$(vacuumpermeability(KKH))

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

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

julia> luminousefficacy(KKH) # lm⋅h³⋅kg⁻¹⋅km⁻²
$(luminousefficacy(KKH))
```
""" KKH

@doc """
    IAU☉ = EntropySystem(Metric,DAY,au,GM☉/G)

Solar `UnitSystem` defined by International Astronomical Union and `solarmass`.

```Julia
julia> boltzmann(IAU) # M⊙⋅au²⋅D⁻²⋅K⁻¹
$(boltzmann(IAU))

julia> planckreduced(IAU) # M⊙⋅au²⋅D⁻¹⋅rad⁻¹
$(planckreduced(IAU))

julia> lightspeed(IAU) # au⋅D⁻¹
$(lightspeed(IAU))

julia> vacuumpermeability(IAU) # M⊙⋅au²⋅C⁻²
$(vacuumpermeability(IAU))

julia> electronmass(IAU) # M⊙
$(electronmass(IAU))

julia> molarmass(IAU) # M☉⋅mol⁻¹
$(molarmass(IAU))

julia> luminousefficacy(IAU) # lm⋅D³⋅M☉⁻¹⋅au⁻²
$(luminousefficacy(IAU))

julia> gaussgravitation(IAU) # D⁻¹
$(gaussgravitation(IAU))
```
""" IAU☉, IAU

@doc """
    IAUE = EntropySystem(Metric,DAY,LD,GME/G)

Astronomical (Earth) `UnitSystem` defined by `lunardistance` around the `earthmass`.

```Julia
julia> boltzmann(IAUE) # ME⋅LD²⋅D⁻²⋅K⁻¹
$(boltzmann(IAUE))

julia> planckreduced(IAUE) # ME⋅LD²⋅D⁻¹⋅rad⁻¹
$(planckreduced(IAUE))

julia> lightspeed(IAUE) # LD⋅D⁻¹
$(lightspeed(IAUE))

julia> vacuumpermeability(IAUE) # ME⋅LD²⋅C⁻²
$(vacuumpermeability(IAUE))

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

julia> molarmass(IAUE) # ME⋅mol⁻¹
$(molarmass(IAUE))

julia> luminousefficacy(IAUE) # lm⋅D³⋅ME⁻¹⋅LD⁻²
$(luminousefficacy(IAUE))

julia> turn(IAU)/gaussianmonth(IAU) # D⁻¹
$(turn(IAU)/gaussianmonth(IAU))
```
""" IAUE

@doc """
    IAUJ = EntropySystem(Metric,DAY,JD,GMJ/G)

Astronomical (Jupiter) `UnitSystem` defined by `jupiterdistance` around the `solarmass`.

```Julia
julia> boltzmann(IAUJ) # MJ⋅JD²⋅D⁻²⋅K⁻¹
$(boltzmann(IAUJ))

julia> planckreduced(IAUJ) # MJ⋅JD²⋅D⁻¹⋅rad⁻¹
$(planckreduced(IAUJ))

julia> lightspeed(IAUJ) # JD⋅D⁻¹
$(lightspeed(IAUJ))

julia> vacuumpermeability(IAUJ) # MJ⋅JD²⋅C⁻²
$(vacuumpermeability(IAUJ))

julia> electronmass(IAUJ) # MJ
$(electronmass(IAUJ))

julia> molarmass(IAUJ) # MJ⋅mol⁻¹
$(molarmass(IAUJ))

julia> luminousefficacy(IAUJ) # lm⋅D³⋅MJ⁻¹⋅JD⁻²
$(luminousefficacy(IAUJ))

julia> sqrt(gravitation(IAUJ)*solarmass(IAUJ)/jupiterdistance(IAUJ)^3) # D⁻¹
$(sqrt(gravitation(IAUJ)*solarmass(IAUJ)/jupiterdistance(IAUJ)^3))
```
""" IAUJ

#=@doc """
    Astronomical = AstronomicalSystem(Metric)

Astronomical `UnitSystem` defined by making the `newton` gravitational constant 1.

```Julia
julia> boltzmann(Astronomical)
$(boltzmann(Astronomical))

julia> planckreduced(Astronomical)
$(planckreduced(Astronomical))

julia> lightspeed(Astronomical)
$(lightspeed(Astronomical))

julia> vacuumpermeability(Astronomical)
$(vacuumpermeability(Astronomical))

julia> electronmass(Astronomical)
$(electronmass(Astronomical))

julia> molarmass(Astronomical)
$(molarmass(Astronomical))

julia> luminousefficacy(Astronomical)
$(luminousefficacy(Astronomical))

julia> gravitation(Astronomical)
$(gravitation(Astronomical))
```
""" Astronomical=#

@doc """
    Hubble = EntropySystem(Metric,th,𝘤*th,𝟏)

Hubble `UnitSystem` defined by `hubble` parameter.

```Julia
julia> boltzmann(Hubble)
$(boltzmann(Hubble))

julia> planckreduced(Hubble)
$(planckreduced(Hubble))

julia> lightspeed(Hubble)
$(lightspeed(Hubble))

julia> vacuumpermeability(Hubble)
$(vacuumpermeability(Hubble))

julia> electronmass(Hubble)
$(electronmass(Hubble))

julia> molarmass(Hubble)
$(molarmass(Hubble))

julia> luminousefficacy(Hubble)
$(luminousefficacy(Hubble))

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

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

@doc """
    Cosmological = EntropySystem(Metric,lc/𝘤,lc,mc)

Cosmological scale `UnitSystem` defined by `darkenergydensity`.

```Julia
julia> boltzmann(Cosmological)
$(boltzmann(Cosmological))

julia> planckreduced(Cosmological)
$(planckreduced(Cosmological))

julia> lightspeed(Cosmological)
$(lightspeed(Cosmological))

julia> vacuumpermeability(Cosmological)
$(vacuumpermeability(Cosmological))

julia> electronmass(Cosmological)
$(electronmass(Cosmological))

julia> molarmass(Cosmological)
$(molarmass(Cosmological))

julia> luminousefficacy(Cosmological)
$(luminousefficacy(Cosmological))

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

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

@doc """
    CosmologicalQuantum = EntropySystem(Metric,tcq,lcq,mcq)

Cosmological quantum scale `UnitSystem` defined by `darkenergydensity`.

```Julia
julia> boltzmann(CosmologicalQuantum)
$(boltzmann(CosmologicalQuantum))

julia> planckreduced(CosmologicalQuantum)
$(planckreduced(CosmologicalQuantum))

julia> lightspeed(CosmologicalQuantum)
$(lightspeed(CosmologicalQuantum))

julia> vacuumpermeability(CosmologicalQuantum)
$(vacuumpermeability(CosmologicalQuantum))

julia> electronmass(CosmologicalQuantum)
$(electronmass(CosmologicalQuantum))

julia> molarmass(CosmologicalQuantum)
$(molarmass(CosmologicalQuantum))

julia> luminousefficacy(CosmologicalQuantum)
$(luminousefficacy(CosmologicalQuantum))
```
""" CosmologicalQuantum

@doc """
    British = RankineSystem(Metric,ft,slug)

British Gravitational `UnitSystem` historically used by Britain and United States.

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

julia> planckreduced(British) # ft⋅lb⋅s⋅rad⁻¹
$(planckreduced(British))

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

julia> vacuumpermeability(British) # slug⋅ft⋅C⁻²
$(vacuumpermeability(British))

julia> electronmass(British) # slugs
$(electronmass(British))

julia> molarmass(British) # slug⋅slug-mol⁻¹
$(molarmass(British))

julia> luminousefficacy(British) # lm⋅s⋅ft⁻¹⋅lb⁻¹
$(luminousefficacy(British))
```
""" British, BritishGravitational, BG

@doc """
    English = RankineSystem(Metric,ft,lb,g₀/ft)

English Engineering `UnitSystem` historically used in the United States of America.

```Julia
julia> boltzmann(English) # ft⋅lbf⋅°R⁻¹
$(boltzmann(English))

julia> planckreduced(English) # ft⋅lbf⋅s⋅rad⁻¹
$(planckreduced(English))

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

julia> vacuumpermeability(English) # lbm⋅ft⋅C⁻²
$(vacuumpermeability(English))

julia> electronmass(English) # lbm
$(electronmass(English))

julia> molarmass(English) # lbm⋅lb-mol⁻¹
$(molarmass(English))

julia> luminousefficacy(English) # lm⋅s⋅ft⁻¹⋅lbf⁻¹
$(luminousefficacy(English))

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

@doc """
    Survey = RankineSystem(Metric,ftUS,lb,g₀/ftUS)

English Engineering `UnitSystem` based on the geophysical US survey foot (1200/3937).

```Julia
julia> boltzmann(Survey) # ftUS⋅lbf⋅°R⁻¹
$(boltzmann(Survey))

julia> planckreduced(Survey) # ftUS⋅lbf⋅s⋅rad⁻¹
$(planckreduced(Survey))

julia> lightspeed(Survey) # ftUS⋅s⁻¹
$(lightspeed(Survey))

julia> vacuumpermeability(Survey) # lbm⋅ftUS⋅C⁻²
$(vacuumpermeability(Survey))

julia> electronmass(Survey) # lbm
$(electronmass(Survey))

julia> molarmass(Survey) # lbm⋅lb-mol⁻¹
$(molarmass(Survey))

julia> luminousefficacy(Survey) # lm⋅s⋅ft⁻¹⋅lbf⁻¹
$(luminousefficacy(Survey))

julia> gravity(Survey) # lbm⋅ftUS⋅lbf⁻¹⋅s⁻²
$(gravity(Survey))
```
""" Survey, EnglishUS

@doc """
    FPS = RankineSystem(Metric,ft,lb)

Absolute English `UnitSystem` based on the foot, pound, second, and poundal.

```Julia
julia> boltzmann(FPS) # ft⋅pdl⋅°R⁻¹
$(boltzmann(FPS))

julia> planckreduced(FPS) # ft⋅pdl⋅s⋅rad⁻¹
$(planckreduced(FPS))

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

julia> vacuumpermeability(FPS) # lb⋅ft⋅C⁻²
$(vacuumpermeability(FPS))

julia> electronmass(FPS) # lb
$(electronmass(FPS))

julia> molarmass(FPS) # lb⋅lb-mol⁻¹
$(molarmass(FPS))

julia> luminousefficacy(FPS) # lm⋅s³⋅lb⁻¹⋅ft⁻²
$(luminousefficacy(FPS))
```
""" FPS, AbsoluteEnglish, AE

@doc """
    IPS = RankineSystem(Metric,ft/𝟐^2/𝟑,lb*g₀*𝟐^2*𝟑/ft)

British Gravitational `UnitSystem` historically used in the United States of America.

```Julia
julia> boltzmann(IPS) # in⋅lb⋅°R⁻¹
$(boltzmann(IPS))

julia> planckreduced(IPS) # in⋅lb⋅s⋅rad⁻¹
$(planckreduced(IPS))

julia> lightspeed(IPS) # in⋅s⁻¹
$(lightspeed(IPS))

julia> vacuumpermeability(IPS) # slinch⋅in⋅C⁻²
$(vacuumpermeability(IPS))

julia> electronmass(IPS) # slinch
$(electronmass(IPS))

julia> molarmass(IPS) # slinch⋅slinch-mol⁻¹
$(molarmass(IPS))

julia> luminousefficacy(IPS) # lm⋅s⋅in⁻¹⋅lb⁻¹
$(luminousefficacy(IPS))
```
""" IPS

@doc """
    FFF = EntropySystem(Metric,𝟐*𝟕*DAY,fur,𝟐*𝟑^2*𝟓*lb,°R,0,𝟏)

Furlong–firkin–fortnight `FFF` is a humorous `UnitSystem` based on unusal impractical units.

```Julia
julia> boltzmann(FFF) # fir⋅fur²⋅ftn⁻²⋅F⁻¹
$(boltzmann(FFF))

julia> planckreduced(FFF) # fir⋅fur²⋅ftn⁻¹⋅rad⁻¹
$(planckreduced(FFF))

julia> lightspeed(FFF) # fur⋅ftn⁻¹
$(lightspeed(FFF))

julia> vacuumpermeability(FFF) # fir⋅fur⋅Inf⁻²
$(vacuumpermeability(FFF))

julia> electronmass(FFF) # fir
$(electronmass(FFF))

julia> molarmass(FFF) # fir⋅fir-mol⁻¹
$(molarmass(FFF))

julia> luminousefficacy(FFF) # lm⋅ftn³⋅fir⁻¹⋅fur⁻²
$(luminousefficacy(FFF))
```
""" FFF

@doc """
    MPH = EntropySystem(FPS,HOUR,mi,𝟏)

Mile-pound-hour specification based on `FPS` absolute `UnitSystem`.

```Julia
julia> boltzmann(MPH) # lbf⋅mi²⋅hr⁻²⋅F⁻¹
$(boltzmann(MPH))

julia> planckreduced(MPH) # lbf⋅mi²⋅hr⁻¹⋅rad⁻¹
$(planckreduced(MPH))

julia> lightspeed(MPH) # mi⋅hr⁻¹
$(lightspeed(MPH))

julia> vacuumpermeability(MPH) # lbm⋅mi⋅C⁻²
$(vacuumpermeability(MPH))

julia> electronmass(MPH) # lbm
$(electronmass(MPH))

julia> molarmass(MPH) # lbm⋅lb-mol⁻¹
$(molarmass(MPH))

julia> luminousefficacy(MPH) # lm⋅h³⋅lb⁻¹⋅mi⁻²
$(luminousefficacy(MPH))
```
""" MPH

@doc """
    Nautical = EntropySystem(Metric,HOUR,nm,em^3,𝟏,τ*𝟑^3/𝟐^10/𝟓^12,milli)

Nautical miles, kilo-earthgram, hour specification based on `Meridian` definition.

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

julia> boltzmann(Nautical) # keg⋅nm²⋅hr⁻²⋅K⁻¹
$(boltzmann(Nautical))

julia> planckreduced(Nautical) # keg⋅nm²⋅hr⁻¹⋅rad⁻¹
$(planckreduced(Nautical))

julia> lightspeed(Nautical) # nm⋅hr⁻¹
$(lightspeed(Nautical))

julia> vacuumpermeability(Nautical) # keg⋅nm⋅eC⁻²
$(vacuumpermeability(Nautical))

julia> electronmass(Nautical) # keg
$(electronmass(Nautical))

julia> molarmass(Nautical) # keg⋅eg-mol⁻¹
$(molarmass(Nautical))

julia> luminousefficacy(Nautical) # lm⋅h³⋅keg⁻¹⋅nm⁻²
$(luminousefficacy(Nautical))
```
""" Nautical

# natural unit system docs

textunits(U,S) = """
```Julia
julia> boltzmann($S)
$(boltzmann(U))

julia> planckreduced($S)
$(planckreduced(U))

julia> lightspeed($S)
$(lightspeed(U))

julia> vacuumpermeability($S)
$(vacuumpermeability(U))

julia> electronmass($S)
$(electronmass(U))
```
"""

@doc """
    Planck = UnitSystem(𝟏,𝟏,𝟏,𝟏,√(𝟐*τ*αG))

Planck `UnitSystem` with the `electronmass` value `√(4π*αG)` using gravitational coupling.

$(textunits(Planck,:Planck))
""" Planck

@doc """
    PlanckGauss = UnitSystem(𝟏,𝟏,𝟏,𝟐*τ,√αG)

Planck (Gauss) `UnitSystem` with `permeability` of `4π` and `electronmass` coupling `√αG`.

$(textunits(PlanckGauss,:PlanckGauss))

The well known `PlanckGauss` values for `length`, `time`, `mass`, and `temperature` are:
```Julia
julia> length(PlanckGauss,Metric) # ℓP
$(length(PlanckGauss,Metric))

julia> time(PlanckGauss,Metric) # tP
$(time(PlanckGauss,Metric))

julia> mass(PlanckGauss,Metric) # mP
$(mass(PlanckGauss,Metric))

julia> temperature(PlanckGauss,Metric) # TP
$(temperature(PlanckGauss,Metric))
```
""" PlanckGauss

@doc """
    Stoney = UnitSystem(𝟏,𝟏/α,𝟏,𝟐*τ,√(αG/α)}

Stoney `UnitSystem` with `permeability` of `4π` and `electronmass` coupling `√(αG/α)`.

$(textunits(Stoney,:Stoney))

The well known `Stoney` values for `length`, `time`, `mass`, and `charge` are:
```Julia
julia> length(Stoney,Metric) # lS
$(length(Stoney,Metric))

julia> time(Stoney,Metric) # tS
$(time(Stoney,Metric))

julia> mass(Stoney,Metric) # mS
$(mass(Stoney,Metric))

julia> charge(Stoney,Metric) # qS
$(charge(Stoney,Metric))
```
""" Stoney

@doc """
    Hartree = UnitSystem(𝟏,𝟏,𝟏/α,𝟐*τ*α^2,𝟏)

Hartree atomic `UnitSystem` with `lightspeed` of `αinv` and `permeability` of `𝟐*τ*α^2`.

$(textunits(Hartree,:Hartree))

The well known `Hartree` atomic unit values for `length`, `time`, `mass`, and `charge` are:
```Julia
julia> length(Hartree,Metric) # lA
$(length(Hartree,Metric))

julia> time(Hartree,Metric) # tA
$(time(Hartree,Metric))

julia> mass(Hartree,Metric) # mA
$(mass(Hartree,Metric))

julia> charge(Hartree,Metric) # qA
$(charge(Hartree,Metric))
```
""" Hartree

@doc """
    Rydberg = UnitSystem(𝟏,𝟏,𝟐/α,τ/𝟐*α^2,𝟏/𝟐)

Rydberg `UnitSystem` with `lightspeed` of `𝟐/α` and `permeability` of `π*α^2`.

$(textunits(Rydberg,:Rydberg))
""" Rydberg

@doc """
    Schrodinger = UnitSystem(𝟏,𝟏,𝟏/α,𝟐*τ*α^2,√(αG/α))

Schrodinger `UnitSystem` with `permeability` of `4π/αinv^2` and `electronmass` of `√(αG*αinv)`.

$(textunits(Schrodinger,:Schrodinger))
""" Schrodinger

@doc """
    Electronic = UnitSystem(𝟏,𝟏/α,𝟏,𝟐*τ,𝟏}

Electronic `UnitSystem` with `planckreduced` of `1/α` and `permeability` of `4π`.

$(textunits(Electronic,:Electronic))
""" Electronic

@doc """
    Natural = UnitSystem(𝟏,𝟏,𝟏,𝟏,𝟏)

Natural `UnitSystem` with all primary constants having unit value.

$(textunits(Natural,:Natural))

The well known `Natural` values for `length`, `time`, `mass`, and `charge` are:
```Julia
julia> length(Natural,Metric)
$(length(Natural,Metric))

julia> time(Natural,Metric)
$(time(Natural,Metric))

julia> mass(Natural,Metric)
$(mass(Natural,Metric))

julia> charge(Natural,Metric)
$(charge(Natural,Metric))
```
""" Natural

@doc """
    NaturalGauss = UnitSystem(𝟏,𝟏,𝟏,𝟐*τ,𝟏}

Natural (Gauss) `UnitSystem` with the Gaussian `permeability` value of `4π`.

$(textunits(NaturalGauss,:NaturalGauss))
""" NaturalGauss

@doc """
    QCD = UnitSystem(𝟏,𝟏,𝟏,𝟏,𝟏/μₚₑ)

Qunatum chromodynamics `UnitSystem` with `electronmass` of `𝟏/μₚₑ` or `𝟏/$μₚₑ`.

$(textunits(QCD,:QCD))

The well known `QCD` values for `length`, `time`, `mass`, and `charge` are:
```Julia
julia> length(QCD,Metric) # lQCD
$(length(QCD,Metric))

julia> time(QCD,Metric) # tQCD
$(time(QCD,Metric))

julia> mass(QCD,Metric) # mQCD
$(mass(QCD,Metric))

julia> charge(QCD,Metric)
$(charge(QCD,Metric))
```
""" QCD

@doc """
    QCDGauss = UnitSystem(𝟏,𝟏,𝟏,𝟐*τ,𝟏/μₚₑ)

Qunatum chromodynamics (Gauss) `UnitSystem` with `electronmass` of `𝟏/μₚₑ`.

$(textunits(QCDGauss,:QCDGauss))

The well known `QCDGauss` values for `length`, `time`, `mass`, and `charge` are:
```Julia
julia> length(QCDGauss,Metric) # lQCD
$(length(QCDGauss,Metric))

julia> time(QCDGauss,Metric) # tQCD
$(time(QCDGauss,Metric))

julia> mass(QCDGauss,Metric) # mQCD
$(mass(QCDGauss,Metric))

julia> charge(QCDGauss,Metric)
$(charge(QCDGauss,Metric))
```
""" QCDGauss

@doc """
    QCDoriginal = UnitSystem(𝟏,𝟏,𝟏,𝟐*τ*α,𝟏/μₚₑ)

Qunatum chromodynamics (original) `UnitSystem` with `permeability` of `4π*α`.

$(textunits(QCDoriginal,:QCDoriginal))

The well known `QCDoriginal` values for `length`, `time`, `mass`, and `charge` are:
```Julia
julia> length(QCDoriginal,Metric) # lQCD
$(length(QCDoriginal,Metric))

julia> time(QCDoriginal,Metric) # tQCD
$(time(QCDoriginal,Metric))

julia> mass(QCDoriginal,Metric) # mQCD
$(mass(QCDoriginal,Metric))

julia> charge(QCDoriginal,Metric)
$(charge(QCDoriginal,Metric))
```
""" QCDoriginal


# ------------ Runtime ------------ #

(d::Type{D})(args::Diffed...) where D <: Distributions.Distribution = d(map(unwrap, args)...)
(d::Type{D})(arg1::Diffed, others...) where D <: Distributions.Distribution = d(unwrap(arg1), others...)

# ------------ Abstract ------------ #

@abstract DiffInterpreter (::Distribution)(args...) = propagate(args...)
@abstract DiffInterpreter (::Type{D})(args...) where D <: Distributions.Distribution = propagate(args...)

@abstract DiffInterpreter rand(::D) where D <: Distribution = Change
@abstract DiffInterpreter rand(::Symbol, args...) = propagate(args...)
@abstract DiffInterpreter rand(::Random._GLOBAL_RNG, ::D) where D <: Distribution = Change
@abstract DiffInterpreter rand(::Symbol, call::Function, args...) = propagate(args...)

@abstract DiffInterpreter trace(::D) where D <: Distribution = Change
@abstract DiffInterpreter trace(::Symbol, args...) = consume(args...)
@abstract DiffInterpreter trace(::Random._GLOBAL_RNG, ::D) where D <: Distribution = Change
@abstract DiffInterpreter trace(::Symbol, call::Function, args...) = consume(args...)
@abstract DiffInterpreter trace(args...) = consume(args...)


############################################################################################
# Models to be used in construction
objectives = [
    Objective(ModelWrapper(MyBaseModel(), myparameter1, FlattenDefault()), data_uv),
    Objective(ModelWrapper(MyBaseModel(), myparameter1, FlattenDefault(; output = Float32)), data_uv)
]
generated = [UpdateFalse(), UpdateTrue()]
#=
iter=1
=#

############################################################################################
## Make model for several parameter types
for iter in eachindex(objectives)
    _obj = objectives[iter]
    _flattentype = _obj.model.info.reconstruct.default.output
    @testset "Kernel construction and propagation, ASMC with MCMC" begin
        ## MCMC
        mcmc = MCMC(NUTS,(:μ, :σ,); stepsize = ConfigStepsize(;stepsizeadaption = UpdateFalse()))
        kerneldefault  = SMCDefault(Ntuning = 5, jittermin = 1, jittermax = 5)
        samplingdefault = SampleDefault(chains = 4)
        smc_c = SMCConstructor(mcmc, SMCDefault(;generated = generated[iter]))
        smc_c(_rng, _obj.model, _obj.data, _obj.temperature, samplingdefault)
        SMC(_rng, mcmc, _obj, kerneldefault, samplingdefault)
        @test BaytesCore.get_sym(mcmc) == BaytesSMC.get_sym(smc_c)
        ## Propose new parameter
        smc = SMC(_rng, mcmc, _obj, SMCDefault(;generated = generated[iter]))
        vals, diagnostics = propose!(_rng, smc, _obj.model, _obj.data)
        @test eltype(diagnostics.ρ) == _flattentype
        vals, diagnostics = propose!(_rng, smc, _obj.model, _obj.data, _obj.temperature, UpdateFalse())
        @test eltype(diagnostics.ρ) == _flattentype
        ## Postprocessing
        generate_showvalues(diagnostics)()
        diagtype = infer(_rng, AbstractDiagnostics, smc, _obj.model, _obj.data)
        @test diagnostics isa diagtype
        TGenerated = BaytesSMC.infer_generated(_rng, smc, _obj.model, _obj.data)
        @test diagnostics.generated isa TGenerated
        diags = Vector{diagtype}(undef, 100)
        for iter in eachindex(diags)
            _, diags[iter] = propose!(_rng, smc, _obj.model, _obj.data)
        end
        results(diags, smc, 2, [.1, .2, .5, .8, .9])
    end
end

@testset "Kernel construction and propagation, ASMC with PMCMC" begin
    ## MCMC
    pfdefault_pmcmc = ParticleFilterDefault(referencing = Ancestral(),)
    mcmcdefault = MCMCDefault(; stepsize = ConfigStepsize(;stepsizeadaption = UpdateFalse()))
    pmcmc = ParticleGibbs(
        ParticleFilterConstructor(keys(myobjective_pf.tagged.parameter), pfdefault_pmcmc),
        MCMCConstructor(NUTS, keys(myobjective_mcmc.tagged.parameter), mcmcdefault)
    )
    ## Propose new parameter
    @test length(myobjective_mcmc.model.val.latent) == length(myobjective_mcmc.data)
    smc = SMC(_rng, pmcmc, myobjective_mcmc)
    vals, diagnostics = propose!(
        _rng,
        smc,
        myobjective_mcmc.model,
        myobjective_mcmc.data,
        myobjective_mcmc.temperature,
        UpdateTrue()
    )

    ## Postprocessing
    diagtype = infer(_rng, AbstractDiagnostics, smc, myobjective_mcmc.model, myobjective_mcmc.data)
    @test diagnostics isa diagtype
    smc = SMC(_rng, pmcmc, myobjective_mcmc)
    diags = Vector{diagtype}(undef, 100)
    for iter in eachindex(diags)
        _, diags[iter] = propose!(
            _rng,
            smc,
            myobjective_mcmc.model,
            myobjective_mcmc.data,
            myobjective_mcmc.temperature,
            UpdateTrue()
        )
    end
    results(diags, smc, 2, [.1, .2, .5, .8, .9])
end

############################################################################################
## Check IBIS
for iter in eachindex(objectives)
    _obj = objectives[iter]
    _flattentype = _obj.model.info.reconstruct.default.output
    @testset "Kernel construction and propagation, IBIS" begin
        ## Assign smc kernel ~ use PMCMC to check both MCMC and Particle Filter
        ## MCMC
        mcmc = MCMC(NUTS,(:μ, :σ,); stepsize = ConfigStepsize(;stepsizeadaption = UpdateFalse()))
        kerneldefault  = SMCDefault(Ntuning = 5, jittermin = 1, jittermax = 5)
        samplingdefault = SampleDefault(chains = 4)
        smc_c = SMCConstructor(mcmc, SMCDefault())

        #Create new objective with small amount of data
        _obj2 = Objective(_obj.model, _obj.data[1:50])
        SMC(_rng, mcmc, _obj2, kerneldefault, samplingdefault)
        @test BaytesCore.get_sym(mcmc) == BaytesSMC.get_sym(smc_c)
        ## Propose new parameter
        smc = SMC(_rng, mcmc, _obj2)
        vals, diagnostics = propose!(_rng, smc, _obj2.model, _obj2.data)
        @test eltype(diagnostics.ρ) == _flattentype
        ## Postprocessing
        smc = SMC(_rng, mcmc, _obj2)
        diagtype = infer(_rng, AbstractDiagnostics, smc, _obj.model, _obj.data)
        @test diagnostics isa diagtype
        datadiff = length(_obj.data) - length(_obj2.data) - 1
        diags = Vector{diagtype}(undef, datadiff)
        for iter in eachindex(diags)
            data_temp = _obj.data[1:(length(_obj2.data) + iter)]
            _, diags[iter] = propose!(_rng, smc, _obj.model, data_temp)
        end
        results(diags, smc, 2, [.1, .2, .5, .8, .9])
    end
end

############################################################################################
## Check SMC2
@testset "Kernel construction and propagation, SMC2" begin
    ## MCMC
    pfdefault_propagate = ParticleFilterDefault(referencing = Marginal(),)
    pfdefault_pmcmc = ParticleFilterDefault(referencing = Ancestral(),)
    mcmcdefault = MCMCDefault(; stepsize = ConfigStepsize(;stepsizeadaption = UpdateFalse()))

    pmcmc = ParticleGibbs(
        ParticleFilterConstructor(keys(myobjective_pf.tagged.parameter), pfdefault_pmcmc),
        MCMCConstructor(NUTS, keys(myobjective_mcmc.tagged.parameter), mcmcdefault)
    )
    pf = ParticleFilterConstructor(keys(myobjective_pf.tagged.parameter), pfdefault_propagate)
    smc2 = SMC2Constructor(pf, pmcmc)
    SMC2(pf, pmcmc)
    @test keys(myobjective_mcmc.model.val) == BaytesSMC.get_sym(smc2)

    ## Propose new parameter and exten tagged propagation parameter
    _obj = Objective(deepcopy(mymodel), data_init)
    @test length(_obj.model.val.latent) == length(_obj.data)  == N_SMC2
    smc = SMC(_rng, smc2, _obj)
    vals, diagnostics = propose!(
        _rng,
        smc,
        _obj.model,
        data[1:N_SMC2+1],
        1.0,
        UpdateTrue()
    )
    @test length(mymodel.val.latent) == N_SMC2
    @test length(_obj.model.val.latent) == N_SMC2 + 1
    ## Postprocessing
    diagtype = infer(_rng, AbstractDiagnostics, smc, _obj.model, _obj.data)
    @test diagnostics isa diagtype

    _obj = Objective(deepcopy(mymodel), data_init)
    smc = SMC(_rng, smc2, _obj)
    diags = Vector{diagtype}(undef, 100)
    for iter in eachindex(diags)
        data_temp = data[1:N_SMC2+iter]
        _, diags[iter] = propose!(
            _rng,
            smc,
            _obj.model,
            data_temp,
            1.0,
            UpdateTrue()
        )
    end
    results(diags, smc, 2, [.1, .2, .5, .8, .9])
end


"""
load xplor or charmm (namd) format dcd file
"""
function readdcd(filename::String; index=nothing, stride=1)
    #TODO: endian
    header_ischarmm_4dims = false
    header_ischarmm_extrablock = false
    x = y = z = Matrix{Float64}(undef, 0, 0)
    boxsize = Matrix{Float64}(undef, 0, 0)

    open(filename, "r") do io
        seekend(io)
        file_size = position(io) # get file size in byte
        seekstart(io)
        blocksize = read(io, Int32) # should be 84
        # header (4 chars) either "CORD" or "VELD"; position(io) == 4 bytes
        header_hdr = Array{Char, 1}(undef, 4)
        for i in 1:4
            header_hdr[i] = read(io, Char)
        end
        # the total # of frames (snapshots); position(io) == 8 bytes
        header_nset = read(io, Int32)
        # starting time-step; position(io) == 12 bytes
        header_istrt = read(io, Int32)
        # frequency to save trajectory; position(io) == 16 bytes
        header_nsavc = read(io, Int32)
        # the total # of simulation step; position(io) == 20 bytes
        header_nstep = read(io, Int32)
        # null4 (int*4); position(io) == 24 bytes
        header_null4 = Array{Int32, 1}(undef, 4)
        read!(io, header_null4)
        # of degrees of freedom; position(io) == 40 bytes
        header_nfreat = read(io, Int32)
        # step-size of simulation; position(io) == 44 bytes
        header_delta = read(io, Float32)
        # null9 (int*9); position(io) == 48 bytes
        header_null9 = Array{Int32, 1}(undef, 9)
        read!(io, header_null9)
        # version; position(io) == 84 bytes
        header_version = read(io, Int32)
        # charmm extension format
        if header_version > 0
            header_ischarmm = true
        end
        # delta is double precision in xplor format
        if header_ischarmm == false
            cof = position(io)
            # reread delta in double precision
            fseek(fid, 44)
            header_delta = read(io, Float64)
            seek(io, cof)
        end
        # check charmm extensions
        if header_ischarmm == true
            cof = position(io)
            # charmm extrablock extension
            seek(io, 48)
            n = read(io, Int32)
            if n == 1
                header_ischarmm_extrablock = true
            end
            # charmm 4dims extension
            seek(io, 52);
            n = read(io, Int32)
            if n == 1
                header_ischarmm_4dims = true
            end
            seek(io, cof)
        end
        # blocksize1; position(io) == 88 bytes
        blocksize1 = read(io, Int32)
        #### read block 2 (title)
        # blocksize2; position(io) == 92 bytes
        blocksize2 = read(io, Int32)
        # of title lines; position(io) == 96 bytes
        header_ntitle = read(io, Int32)
        # title
        header_title = []
        for i in 1:header_ntitle
            t = Array{Char, 1}(undef, 80)
            for i in 1:80
                t[i] = read(io, Char)
            end
            push!(header_title, String(t))
        end
        # blocksize2
        blocksize2 = read(io, Int32)
        #### read block 3 (natom)
        # blocksize3
        blocksize3 = read(io, Int32)
        # # of atoms
        header_natom = read(io, Int32)
        # blocksize3
        blocksize3 = read(io, Int32)
        ## read coordinates
        header_size = position(io)
        if header_ischarmm_extrablock == true
            extrablocksize = 4*2 + 8*6
        else
            extrablocksize = 0
        end
        coordblocksize = (4*2 + 4*header_natom)*3
        nframe = (file_size - header_size) / (extrablocksize + coordblocksize)
        nframe = Int64(floor(nframe))
        nframe_stride = length(1:stride:nframe)
        if typeof(index) <: AbstractVector{Bool}
            index2 = findall(index)
        elseif index == nothing
            index2 = collect(1:header_natom)
        else
            index2 = index
        end
        x = zeros(Float64, (nframe_stride, length(index2)))
        y = zeros(Float64, (nframe_stride, length(index2)))
        z = zeros(Float64, (nframe_stride, length(index2)))
        if header_ischarmm_extrablock == true
            boxsize = zeros(Float64, (nframe_stride, 3))
        end
        # read next frames
        dummy = Array{Float64, 1}(undef, 6)
        crd_x = Array{Float32, 1}(undef, header_natom)
        crd_y = Array{Float32, 1}(undef, header_natom)
        crd_z = Array{Float32, 1}(undef, header_natom)
        for iframe in 1:nframe_stride
            # read charmm extrablock (unitcell info)
            if header_ischarmm_extrablock == true
                blocksize_box = read(io, Int32)
                read!(io, dummy)
                blocksize_box = read(io, Int32)
            end
            # read x coordinates
            blocksize_x = read(io, Int32)
            read!(io, crd_x)
            blocksize_x = read(io, Int32)
            # read y coordinates
            blocksize_y = read(io, Int32)
            read!(io, crd_y)
            blocksize_y = read(io, Int32)
            # read z coordinates
            blocksize_z = read(io, Int32)
            read!(io, crd_z)
            blocksize_z = read(io, Int32)
            # skip charmm 4dims extension
            if header_ischarmm_4dims == true
                blocksize_4dims = read(io, Int32)
                skip(io, blocksize_4dims)
                blocksize_4dims = read(io, Int32)
            end
            # get data
            x[iframe, :] = crd_x[index2];
            y[iframe, :] = crd_y[index2];
            z[iframe, :] = crd_z[index2];
            if header_ischarmm_extrablock == true
                boxsize[iframe, :] = dummy[[1 3 6]];
            end

            # skip stride-1 frames
            if iframe < nframe_stride
                for ii = 1:(stride-1)
                    if header_ischarmm_extrablock == true
                        blocksize_box = read(io, Int32)
                        skip(io, blocksize_box)
                        blocksize_box = read(io, Int32)
                    end
                    # read x coordinates
                    blocksize_x = read(io, Int32)
                    skip(io, blocksize_x)
                    blocksize_x = read(io, Int32)
                    # read y coordinates
                    blocksize_y = read(io, Int32)
                    skip(io, blocksize_y)
                    blocksize_y = read(io, Int32)
                    # read z coordinates
                    blocksize_z = read(io, Int32)
                    skip(io, blocksize_z)
                    blocksize_z = read(io, Int32)
                    # skip charmm 4dims extension
                    if header_ischarmm_4dims == true
                        blocksize_4dims = read(io, Int32)
                        skip(io, blocksize_4dims)
                        blocksize_4dims = read(io, Int32)
                    end
                end
            end
        end
    end

    if isempty(boxsize)
        TrjArray{Float64, Int64}(x=x, y=y, z=z)
    else
        TrjArray{Float64, Int64}(x=x, y=y, z=z, boxsize=boxsize)
    end
end

############################################################################
"""
read netcdf file
"""
function readnetcdf(filename::String; index=nothing)
    #finfo = ncinfo(filename)
    #attributes = finfo.gatts
    #dimensions = finfo.dims
    nc = NetCDF.open(filename, mode=NetCDF.NC_64BIT_OFFSET);
    nframe = Int64(nc.dim["frame"].dimlen)
    natom = Int64(nc.dim["atom"].dimlen)

    is_trj = haskey(nc.vars, "coordinates") ? true : false
    is_box = haskey(nc.vars, "cell_lengths") ? true : false
    is_vel = haskey(nc.vars, "velocities") ? true : false
    is_temp = haskey(nc.vars, "temp0") ? true : false

    NetCDF.close(nc)

    if typeof(index) <: AbstractVector{Bool}
        index2 = findall(index)
    elseif index == nothing
        index2 = collect(1:natom)
    else
        index2 = index
    end

    start_atom = minimum(index2)
    count_atom = maximum(index2) - start_atom + 1
    index3 = index2 .- start_atom .+ 1
    start_time = 1
    count_time = nframe

    if is_trj
        d = ncread(filename, "coordinates", start=[1, start_atom, start_time], count=[3, count_atom, count_time])
        d = map(Float64, d)
        x = d[1, index3, :]'
        y = d[2, index3, :]'
        z = d[3, index3, :]'
    else
        x = y = z = Matrix{Float64}(undef, 0, 0)
    end

    if is_box
        d = ncread(filename, "cell_lengths", start=[1, start_time], count=[3, count_time]);
        d = map(Float64, d)
        boxsize = d'
    else
        boxsize = Matrix{Float64}(undef, 0, 0)
    end

    if is_vel
        d = ncread(filename, "velocities", start=[1, start_atom, start_time], count=[3, count_atom, count_time])
        d = map(Float64, d)
        vx = d[1, index3, :]'
        vy = d[2, index3, :]'
        vz = d[3, index3, :]'
    else
        vx = vy = vz = Matrix{Float64}(undef, 0, 0)
    end

    if is_temp
        d = ncread(filename, "temp0", start=[start_time], count=[count_time]);
        temp = map(Float64, d)
    else
        temp = Vector{Float64}(undef, 0)
    end

    TrjArray{Float64, Int64}(x=x, y=y, z=z, boxsize=boxsize)
end

############################################################################
"""
write netcdf file
"""
function writenetcdf(filename::String, ta::TrjArray; velocity = nothing, force = nothing)
    scale_factor = 20.455
    natom = ta.natom
    nframe = ta.nframe

    # global attributes
    gatts = Dict(
        "ConventionVersion" => "1.0",
        "Conventions" => "AMBER",
        "application" => "Julia",
        "program" => "MDToolbox.jl",
        "programVersion" => "0.1",
        "title" => "Created by MDToolbox.jl"
    )

    # dimensions
    ncdim_frame = NcDim("frame", nframe, unlimited=true)
    ncdim_atom = NcDim("atom", natom)
    ncdim_spatial = NcDim("spatial", 3)
    ncdim_cell_spatial = NcDim("cell_spatial", 3)
    ncdim_cell_angular = NcDim("cell_angular", 3)
    ncdim_label = NcDim("label", 5)

    # variables
    ncvar_spatial = NcVar("spatial", ncdim_spatial, t=NetCDF.NC_CHAR)
    ncvar_time = NcVar("time", ncdim_frame, t=NetCDF.NC_FLOAT, atts=Dict("units" => "picosecond"))
    ncvar_coordinates = NcVar("coordinates", [ncdim_spatial, ncdim_atom, ncdim_frame], t=NetCDF.NC_FLOAT, atts=Dict("units" => "angstrom"))
    if !isempty(ta.boxsize)
        ncvar_cell_spatial = NcVar("cell_spatial", ncdim_cell_spatial, t=NetCDF.NC_CHAR)
        ncvar_cell_angular = NcVar("cell_angular", [ncdim_label, ncdim_cell_angular], t=NetCDF.NC_CHAR)
        ncvar_cell_lengths = NcVar("cell_lengths", [ncdim_cell_spatial, ncdim_frame], t=NetCDF.NC_FLOAT, atts=Dict("units" => "angstrom"))
        ncvar_cell_angles = NcVar("cell_angles", [ncdim_cell_angular, ncdim_frame], t=NetCDF.NC_FLOAT, atts=Dict("units" => "degree"))
    end
    if velocity != nothing
        ncvar_velocities = NcVar("velocities", [ncdim_spatial, ncdim_atom, ncdim_frame], t=NetCDF.NC_FLOAT, atts=Dict("units" => "angstrom/picosecond"))
    end
    if force != nothing
        ncvar_forces = NcVar("forces", [ncdim_spatial, ncdim_atom, ncdim_frame], t=NetCDF.NC_FLOAT, atts=Dict("units" => "amu*angstrom/picosecond^2"))
    end
    #ncvar_temp0 = NcVar("temp0", ncdim_frame, t=NetCDF.NC_FLOAT, atts=Dict("units" => "kelvin"))

    # create the NetCDF file
    varlist = [ncvar_spatial, ncvar_time, ncvar_coordinates]
    if !isempty(ta.boxsize)
        push!(varlist, ncvar_cell_spatial)
        push!(varlist, ncvar_cell_angular)
        push!(varlist, ncvar_cell_lengths)
        push!(varlist, ncvar_cell_angles)
    end
    if velocity != nothing
        push!(varlist, ncvar_velocities)
    end
    if force != nothing
        push!(varlist, ncvar_forces)
    end
    nc = NetCDF.create(filename, varlist, gatts=gatts, mode=NetCDF.NC_64BIT_OFFSET)

    # write data
    # check by ncread("tmp.nc", "spatial")
    c = zeros(UInt8, 3); copyto!(c, 1, "xyz", 1); #@show c
    NetCDF.putvar(nc, "spatial", c)
    NetCDF.putvar(nc, "time", zeros(Float32, nframe))
    trj = zeros(Float32, 3, natom, nframe)
    for iframe in 1:nframe
        for iatom in 1:natom
            trj[1, iatom, iframe] = ta.x[iframe, iatom]
            trj[2, iatom, iframe] = ta.y[iframe, iatom]
            trj[3, iatom, iframe] = ta.z[iframe, iatom]
        end
    end
    NetCDF.putvar(nc, "coordinates", trj)

    if !isempty(ta.boxsize)
        c = zeros(UInt8, 3); copyto!(c, 1, "abc", 1); #@show c
        NetCDF.putvar(nc, "cell_spatial", c)
        c = zeros(UInt8, 5, 3); copyto!(c, 1, "alpha", 1); copyto!(c, 6, "beta ", 1); copyto!(c, 11, "gamma", 1); #@show c
        NetCDF.putvar(nc, "cell_angular", c)
        d = zeros(Float32, 3, nframe)
        d[:, :] .= ta.boxsize'
        NetCDF.putvar(nc, "cell_lengths", d)
        d[:, :] .= 90.0
        NetCDF.putvar(nc, "cell_angles", d)
    end

    if velocity != nothing
        for iframe in 1:nframe
            for iatom in 1:natom
                trj[1, iatom, iframe] = velocity.x[iframe, iatom]/scale_factor
                trj[2, iatom, iframe] = velocity.y[iframe, iatom]/scale_factor
                trj[3, iatom, iframe] = velocity.z[iframe, iatom]/scale_factor
            end
        end
        NetCDF.putvar(nc, "velocities", trj)
    end
    if force != nothing
        for iframe in 1:nframe
            for iatom in 1:natom
                trj[1, iatom, iframe] = force.x[iframe, iatom]; #TODO: scale_factor?
                trj[2, iatom, iframe] = force.y[iframe, iatom]
                trj[3, iatom, iframe] = force.z[iframe, iatom]
            end
        end
        NetCDF.putvar(nc, "forces", trj)
    end

    NetCDF.close(nc)
end

############################################################################
function parse_line(line::String, index, mytype::DataType, default_value)
    try
        if mytype == String
            return strip(string(line[index]))
        else
            return parse(mytype, line[index])
        end
    catch
        return default_value
    end
end

############################################################################
"""
read charmm or xplor type psf file
"""
function readpsf(filename::String)
    isPSF = false
    isEXT = false
    isCMAP = false
    isCHEQ = false

    lines = open(filename, "r" ) do fp
        readlines(fp)
    end

    # first line
    line = lines[1]
    line_size = length(line)
    if line_size >= 3
        if line[1:3] == "PSF"
            isPSF = true
        end
    end
    for i = 1:4:line_size
        i_max = minimum([i+3, line_size])
        isPSF = strip(line[i:i_max]) == "PSF" ? true : isPSF
        isEXT = strip(line[i:i_max]) == "EXT" ? true : isEXT
        isCMAP = strip(line[i:i_max]) == "CMAP" ? true : isCMAP
        isCHEQ = strip(line[i:i_max]) == "CHEQ" ? true : isCHEQ
    end
    if !isPSF
        print("Sorry, this seems not be a PSF file")
        return false
    end
    if isEXT
        #fmt_atom = "%10d| %8s| %8d| %8s| %8s| %4s| %14f|%14f|%8d %*f %*f" %octave
        fmt_atom = [1:10, 12:19, 21:28, 30:37, 39:46, 48:51, 53:66, 67:80, 81:88]
        #fmt_list = "%10d"
    else
        #fmt_atom = "%8d %4s %4d %4s| %4s| %4s| %14f%14f%8d %*f %*f" #octave
        fmt_atom = [1:8, 10:13, 15:18, 20:23, 25:28, 30:33, 34:47, 48:61, 62:69]
        #fmt_list = "%8d"
        fmt_list = "%8d"
    end

    psf_title = Vector{String}(undef, 0)
    psf_segment_name = Vector{String}(undef, 0)
    psf_residue_name = Vector{String}(undef, 0)
    psf_residue_id = Vector{Int64}(undef, 0)
    psf_atom_name = Vector{String}(undef, 0)
    psf_atom_type_letter = Vector{String}(undef, 0)
    psf_atom_type_number = Vector{Int64}(undef, 0)
    psf_atom_id = Vector{Int64}(undef, 0)
    psf_charge = Vector{Float64}(undef, 0)
    psf_mass = Vector{Float64}(undef, 0)
    list_bond = Array{Int64}(undef, 0, 0)
    list_angle = Array{Int64}(undef, 0, 0)
    list_dihedral = Array{Int64}(undef, 0, 0)
    list_improper = Array{Int64}(undef, 0, 0)
    list_cmap = Array{Int64}(undef, 0, 0)

    iline = 2
    while iline <= length(lines)
        line = lines[iline]; iline += 1

        if occursin(r".*\d.*!\w.*", line) && !occursin("NGRP", line)
            line_splitted = split(line, "!")
            #num = parse(Int64, line_splitted[1])
            num = parse_line(String(line_splitted[1]), 1:length(line_splitted[1]), Int64, 0)
            key = strip(line_splitted[2])
            if occursin(r"^NATOM", key)
                for ii = iline:(iline+num-1)
                    line = lines[ii]
                    push!(psf_atom_id, parse_line(line, fmt_atom[1], Int64, 0))
                    push!(psf_segment_name, parse_line(line, fmt_atom[2], String, "None"))
                    push!(psf_residue_id, parse_line(line, fmt_atom[3], Int64, 0))
                    push!(psf_residue_name, parse_line(line, fmt_atom[4], String, "None"))
                    push!(psf_atom_name, parse_line(line, fmt_atom[5], String, "None"))
                    if isletter(strip(line[fmt_atom[6]])[1])
                        push!(psf_atom_type_letter, parse_line(line, fmt_atom[6], String, "None")) # CHARMM format
                    else
                        push!(psf_atom_type_number, parse_line(line, fmt_atom[6], Int64, 0)) # XPLOR format
                    end
                    push!(psf_charge, parse_line(line, fmt_atom[7], Float64, 0.0))
                    push!(psf_mass, parse_line(line, fmt_atom[8], Float64, 0.0))
                end
                iline += num

            elseif occursin(r"^NBOND", key)
                a = Vector{Int64}(undef, 0)
                while length(a) < (num*2)
                    line = lines[iline]; iline += 1
                    append!(a, [parse(Int64, y) for y in split(line)])
                end
                list_bond = collect(reshape(a, 2, :)')

            elseif occursin(r"^NTHETA", key)
                a = Vector{Int64}(undef, 0)
                while length(a) < (num*3)
                    line = lines[iline]; iline += 1
                    append!(a, [parse(Int64, y) for y in split(line)])
                end
                list_angle = collect(reshape(a, 3, :)')

            elseif occursin(r"^NPHI", key)
                a = Vector{Int64}(undef, 0)
                while length(a) < (num*4)
                    line = lines[iline]; iline += 1
                    append!(a, [parse(Int64, y) for y in split(line)])
                end
                list_dihedral = collect(reshape(a, 4, :)')

            elseif occursin(r"^NIMPHI", key)
                a = Vector{Int64}(undef, 0)
                while length(a) < (num*4)
                    line = lines[iline]; iline += 1
                    append!(a, [parse(Int64, y) for y in split(line)])
                end
                list_improper = collect(reshape(a, 4, :)')

            elseif occursin(r"^NCRTERM", key)
                a = Vector{Int64}(undef, 0)
                while length(a) < (num*8)
                    line = lines[iline]; iline += 1
                    append!(a, [parse(Int64, y) for y in split(line)])
                end
                list_cmap = collect(reshape(a, 8, :)')

            end
        end
    end

    TrjArray{Float64, Int64}(chainname=psf_segment_name,
             resname=psf_residue_name, resid=psf_residue_id,
             atomname=psf_atom_name, atomid=psf_atom_id,
             mass=psf_mass, charge=psf_charge,
             list_bond=list_bond, list_angle=list_angle,
             list_dihedral=list_dihedral, list_improper=list_improper,
             list_cmap=list_cmap)
end

############################################################################
"""
write psf file
"""
function writepsf(filename::String, ta::TrjArray)
    natom = ta.natom

    open(filename, "w") do io
        Printf.@printf(io, "%4s", "PSF ")
        if !isempty(ta.list_cmap)
            Printf.@printf(io, "%4s", "CMAP")
        end
        Printf.@printf(io, "\n\n")

        Printf.@printf(io, "%8d !NTITLE\n", 1)
        Printf.@printf(io, "CREATED by MDToolbox.jl\n")
        Printf.@printf(io, "\n")

        Printf.@printf(io, "%8d !NATOM\n", natom)
        for iatom = 1:natom
            Printf.@printf(io, "%8d", iatom)
            Printf.@printf(io, " %4s", isempty(ta.chainname) ? rpad("NONE", 4) : rpad(ta.chainname[iatom], 4))
            Printf.@printf(io, " %-4d", isempty(ta.resid) ? 0 : ta.resid[iatom])
            Printf.@printf(io, " %4s", isempty(ta.resname) ? rpad("NONE", 4) : rpad(ta.resname[iatom], 4))
            Printf.@printf(io, " %4s", isempty(ta.atomname) ? rpad("NONE", 4) : rpad(ta.atomname[iatom], 4))
            Printf.@printf(io, " %4s", isempty(ta.atomname) ? rpad("NONE", 4) : rpad(ta.atomname[iatom], 4))
            Printf.@printf(io, "%14.9f", isempty(ta.charge) ? 0.0 : ta.charge[iatom])
            Printf.@printf(io, "%14.7f", isempty(ta.mass) ? 0.0 : ta.mass[iatom])
            Printf.@printf(io, "%9d", 0)
            Printf.@printf(io, "\n")
        end

        if !isempty(ta.list_bond)
            Printf.@printf(io, "%8d !NBOND\n", size(ta.list_bond, 1))
            for i = 1:size(ta.list_bond, 1)
                for j = 1:size(ta.list_bond, 2)
                    Printf.@printf(io, "%8d", ta.list_bond[i, j])
                end
                if (i % 4) == 0
                    Printf.@printf(io, "\n")
                end
            end
            Printf.@printf(io, "\n")
        end

        if !isempty(ta.list_angle)
            Printf.@printf(io, "%8d !NTHETA\n", size(ta.list_angle, 1))
            for i = 1:size(ta.list_angle, 1)
                for j = 1:size(ta.list_angle, 2)
                    Printf.@printf(io, "%8d", ta.list_angle[i, j])
                end
                if (i % 3) == 0
                    Printf.@printf(io, "\n")
                end
            end
            Printf.@printf(io, "\n")
        end

        if !isempty(ta.list_dihedral)
            Printf.@printf(io, "%8d !NPHI\n", size(ta.list_dihedral, 1))
            for i = 1:size(ta.list_dihedral, 1)
                for j = 1:size(ta.list_dihedral, 2)
                    Printf.@printf(io, "%8d", ta.list_dihedral[i, j])
                end
                if (i % 2) == 0
                    Printf.@printf(io, "\n")
                end
            end
            Printf.@printf(io, "\n")
        end

        if !isempty(ta.list_improper)
            Printf.@printf(io, "%8d !NIMPHI\n", size(ta.list_improper, 1))
            for i = 1:size(ta.list_improper, 1)
                for j = 1:size(ta.list_improper, 2)
                    Printf.@printf(io, "%8d", ta.list_improper[i, j])
                end
                if (i % 2) == 0
                    Printf.@printf(io, "\n")
                end
            end
            Printf.@printf(io, "\n")
        end

        if !isempty(ta.list_cmap)
            Printf.@printf(io, "%8d !NCRTERM\n", size(ta.list_cmap, 1))
            for i = 1:size(ta.list_cmap, 1)
                for j = 1:size(ta.list_cmap, 2)
                    Printf.@printf(io, "%8d", ta.list_cmap[i, j])
                end
                Printf.@printf(io, "\n")
            end
            Printf.@printf(io, "\n")
        end
    end
end

############################################################################
"""
read protein data bank (PDB) file
"""
function readpdb(filename::String)
    lines = open(filename, "r" ) do fp
        readlines(fp)
    end

    model = []
    lines_clean = []
    for i = 1:length(lines)
        line = lines[i]
        if match(r"^ATOM", line) != nothing
            push!(lines_clean, line)
        end
        if match(r"^HETATM", line) != nothing
            push!(lines_clean, line)
        end
        if match(r"^ENDMDL", line) != nothing || (i == length(lines) && !isempty(lines_clean))
            push!(model, lines_clean)
            lines_clean = []
        end
    end

    natom = length(model[1])
    pdb_record = Vector{String}(undef, natom)
    pdb_serial = Vector{Int64}(undef, natom)
    pdb_name = Vector{String}(undef, natom)
    pdb_altloc = Vector{String}(undef, natom)
    pdb_resname = Vector{String}(undef, natom)
    pdb_chainid = Vector{String}(undef, natom)
    pdb_resseq = Vector{Int64}(undef, natom)
    pdb_x = zeros(Float64, length(model), natom)
    pdb_y = zeros(Float64, length(model), natom)
    pdb_z = zeros(Float64, length(model), natom)
    pdb_occupancy = Vector{Float64}(undef, natom)
    pdb_tempfactor = Vector{Float64}(undef, natom)
    pdb_element = Vector{String}(undef, natom)
    pdb_charge = Vector{Float64}(undef, natom)

    lines = model[1]
    for iatom = 1:natom
        line = lines[iatom]
        pdb_record[iatom] = parse_line(line, 1:6, String, "None")
        num = parse_line(line, 7:12, String, "0")
        if iatom == 1 || pdb_serial[iatom-1] < 99999
            pdb_serial[iatom] = parse(Int64, num)
        elseif match(r"\*", num) != nothing
            pdb_serial[iatom] = iatom == 1 ? 1 : pdb_serial[iatom-1]
        else
            pdb_serial[iatom] = parse(Int64, num)
        end
        pdb_name[iatom] = parse_line(line, 13:16, String, "None")
        pdb_altloc[iatom] = parse_line(line, 17:17, String, "None")
        pdb_resname[iatom] = parse_line(line, 18:21, String, "None")
        pdb_chainid[iatom] = parse_line(line, 22:22, String, "None")
        pdb_resseq[iatom] = parse_line(line, 23:28, Int64, 0)
        pdb_x[1, iatom] = parse_line(line, 31:38, Float64, 0.0)
        pdb_y[1, iatom] = parse_line(line, 39:46, Float64, 0.0)
        pdb_z[1, iatom] = parse_line(line, 47:54, Float64, 0.0)
        pdb_occupancy[iatom] = parse_line(line, 55:60, Float64, 0.0)
        pdb_tempfactor[iatom] = parse_line(line, 61:66, Float64, 0.0)
        pdb_element[iatom] = parse_line(line, 77:78, String, "None")
        pdb_charge[iatom] = parse_line(line, 79:80, Float64, 0.0)
    end

    for imodel = 2:length(model)
        lines = model[imodel]
        for iatom = 1:natom
            line = lines[iatom]
            pdb_x[imodel, iatom] = parse_line(line, 31:38, Float64, 0.0)
            pdb_y[imodel, iatom] = parse_line(line, 39:46, Float64, 0.0)
            pdb_z[imodel, iatom] = parse_line(line, 47:54, Float64, 0.0)
        end
    end

    TrjArray{Float64, Int64}(x=pdb_x, y=pdb_y, z=pdb_z,
             chainname=pdb_chainid,
             resid=pdb_resseq, resname=pdb_resname,
             atomid=pdb_serial, atomname=pdb_name)
end

############################################################################
"""
write pdb file
"""
function writepdb(io::IO, ta::TrjArray; format_type="vmd")
    natom = ta.natom
    nframe = ta.nframe

    if format_type == "namd"
        Printf.@printf io "CRYST1    0.000    0.000    0.000  90.00  90.00  90.00 P 1           1\n"
    end

    for iframe = 1:nframe
        for iatom = 1:natom
            Printf.@printf(io, "%6s", "ATOM  ")
            if isempty(ta.atomid)
                Printf.@printf(io, "%5d", mod(iatom, 100000))
            else
                Printf.@printf(io, "%5d", mod(ta.atomid[iatom], 100000))
            end
            Printf.@printf(io, "%1s", " ")
            if isempty(ta.atomname)
                Printf.@printf(io, "%4s", " CA ")
            else
                Printf.@printf(io, "%4s", rpad(ta.atomname[iatom], 3))
            end
            Printf.@printf(io, "%1s", " ")
            #Printf.@printf(io, "%3s", ta.resname(iatom, :))
            #Printf.@printf(io, "%1s", " ")
            if isempty(ta.resname)
                Printf.@printf(io, "%4s", "ALA ")
            else
                Printf.@printf(io, "%4s", rpad(ta.resname[iatom], 4))
            end
            if isempty(ta.chainname)
                Printf.@printf(io, "%1s", " ")
            else
                Printf.@printf(io, "%1s", ta.chainname[iatom])
            end
            if isempty(ta.chainname)
                Printf.@printf(io, "%4d", mod(iatom, 10000))
            else
                Printf.@printf(io, "%4d", mod(ta.resid[iatom], 10000))
            end
            #Printf.@printf(io, "%1s", ta.icode(iatom, :))
            Printf.@printf(io, "%1s", " ")
            Printf.@printf(io, "%1s", " ")
            Printf.@printf(io, "%1s", " ")
            Printf.@printf(io, "%1s", " ")
            #Printf.@printf(io, "%8.3f", ta.xyz(iatom, 1))
            #Printf.@printf(io, "%8.3f", ta.xyz(iatom, 2))
            #Printf.@printf(io, "%8.3f", ta.xyz(iatom, 3))
            Printf.@printf(io, "%8.3f", ta.x[iframe, iatom])
            Printf.@printf(io, "%8.3f", ta.y[iframe, iatom])
            Printf.@printf(io, "%8.3f", ta.z[iframe, iatom])
            Printf.@printf(io, "%6.2f", 0.0)
            Printf.@printf(io, "%6.2f", 0.0)
            Printf.@printf(io, "%1s", " ")
            Printf.@printf(io, "%1s", " ")
            Printf.@printf(io, "%1s", " ")
            Printf.@printf(io, "%1s", " ")
            Printf.@printf(io, "%1s", " ")
            Printf.@printf(io, "%1s", " ")
            Printf.@printf(io, "%1s", " ")
            Printf.@printf(io, "%1s", " ")
            Printf.@printf(io, "%1s", " ")
            Printf.@printf(io, "%1s", " ")
            Printf.@printf(io, "%2s", "  "); #element
            Printf.@printf(io, "%2s", "  "); #charge
            Printf.@printf(io, "\n")
        end
        if nframe > 1
            Printf.@printf(io, "TER\n");
            Printf.@printf(io, "ENDMDL\n");
        end
        #Printf.@printf " %8.2f" X[i,j]
    end
end

"""
write pdb file
"""
function writepdb(filename::String, ta::TrjArray; format_type="vmd")
    open(filename, "w") do io
        writepdb(io, ta, format_type=format_type)
    end
end

############################################################################
"""
read amber coordinates file
"""
function readcrd(filename::String)
    lines = open(filename, "r" ) do fp
        readlines(fp)
    end

    title = lines[1]
    natom = parse_line(lines[2], 1:length(lines[2]), Int64, 0)
    #pdb_name[iatom] = parse_line(line, 13:16, String, "None")

    xyz = Vector{Float64}(undef, natom*3)

    iatom3 = 1
    icount = 3
    while true
        n = length(lines[icount])
        for i = 1:12:n
            xyz[iatom3] = parse_line(lines[icount], i:(i+11), Float64, 0.0)
            iatom3 += 1
        end
        icount += 1
        if iatom3 > (natom*3)
            break
        end
    end

    x = zeros(Float64, 1, natom)
    y = zeros(Float64, 1, natom)
    z = zeros(Float64, 1, natom)
    x[:] .= xyz[1:3:end]
    y[:] .= xyz[2:3:end]
    z[:] .= xyz[3:3:end]

    TrjArray{Float64, Int64}(x=x, y=y, z=z)
end


const UINT64_MAX = typemax(UInt64)
const SIZE_MAX = typemax(UInt32)
const crypto_pwhash_argon2i_MEMLIMIT_MAX = ( ( SIZE_MAX >= 4398046510080 ) ? 4398046510080 : ( SIZE_MAX >= Cuint(2147483648) ) ? Cuint(2147483648) : Cuint(32768) )
const crypto_pwhash_argon2id_MEMLIMIT_MAX = ( ( SIZE_MAX >= 4398046510080 ) ? 4398046510080 : ( SIZE_MAX >= Cuint(2147483648) ) ? Cuint(2147483648) : Cuint(32768) )


using Random
#Implementing the general Advantage Estimation according to https://arxiv.org/abs/1506.02438

#Return the Generalized Advantage Estimation:
@everywhere function gae!(params::Params,traj::Trajectory)
    l_traj = traj.len
    γ      = params.γ_disc
    λ      = params.λ_actor

    Val = traj.val_t
    r_t = traj.r_t

    #Calculate the local TD-error:
    delta_V_i = zeros(l_traj)
    for i_t = 1:l_traj-1
        delta_V_i[i_t] = -Val[i_t] + r_t[i_t] + γ*Val[i_t+1]
    end

    #Discount the TD-Error as the Advantage
    A_t = vec(discount(delta_V_i,λ*γ))

    #Write result in trajectory
    traj.A_t = A_t
end

#Parallel PPO update
#-------------------------------------------------------------------------------
"""
Function training the Value function on the last batch of data in parallel
"""
function refit_VF(params::Params,θv::Any,all_traj::Traj_Data, opt_para_v)
        #Take all partial traj and shuffle their order!
        n_traj = all_traj.n_traj
        shuffle_arr = Random.shuffle(1:n_traj)

        active_workers = workers()
        n_workers = length(active_workers)
        ĝ_v = copy(θv)

        # Create Channels storing results and starting values
        input_channel  = RemoteChannel(()->Channel{Tuple}(length(workers())+1)) # will contain networks and traj
        output_channel = RemoteChannel(()->Channel{Array{Any,1}}(length(workers())+1)) # will contain networks

        # Create problem on every worker
        begin
            for p in workers()
                remote_do(distributed_vf_training, p, params, input_channel, output_channel)
            end
        end

        # Start optimizations
        n_parallel = Int(ceil(n_traj / n_workers))
        prog = Progress(n_traj,1)

        for i_parallel = 1 : n_parallel

            #1. Send the work tasks:
            for i_w = 1 : n_workers
                i_true = ((i_parallel-1)*n_workers + i_w) #get true order
                if i_true <= n_traj
                    i_traj = shuffle_arr[i_true] #shuffle order
                    traj_local = all_traj.traj[i_traj]
                    @async put!(input_channel, (traj_local, θv))
                end
            end

            #2. Take the result
            ĝ_v *= 0
            for i_w = 1: n_workers
                i_true = ((i_parallel-1)*n_workers + i_w)
                if i_true <= n_traj
                    #Take from Channel
                    ĝ_v += take!(output_channel)
                end
            end

            #Update the Network
            Knet.update!(θv, ĝ_v, opt_para_v)
        end

        # Tell workers that its done
        NaN_mat = zeros(length(params.batch_size))
        fill!(NaN_mat,NaN)
        ending_traj = init_traj(params,params.batch_size)
        ending_traj.r_t = NaN_mat

        for j = 1 : n_workers
                @async put!(input_channel, (ending_traj, θv))
        end

        #Close Channels
        sleep(0.1)
        close(input_channel)
        close(output_channel)

        #Return the updated Network
        return θv
end

# Function running on the worker
@everywhere function distributed_vf_training(params::Params, input_channel::RemoteChannel, output_channel::RemoteChannel)

    # Start loop where work is done
    while true
        # break loop if NaN state received
        try
            #Take the new Input
            input = take!(input_channel)

            #Reorder data:
            traj_local = input[1]
            θv         = input[2]

            #Check if u reached final
            if (sum(isnan, traj_local.r_t) > 0)
                      break
            end

            #DO THE WORK:
            #-----------------------------------------------------------------------
            #Calculate the Value Function gradient using TD(1)
            ĝ_v_loc = grad_value(θv,params,traj_local)
            #-----------------------------------------------------------------------

            put!(output_channel, ĝ_v_loc)

        catch
            println("Some problem occured, worker $(myid()) exiting trajectory optimization function.")
            println(traj_local)
            break
        end
    end

    return nothing
end

#Function applying general advantage estimation for each trajectory
function gae_all!(all_traj::Traj_Data,θv::Any,params::Params)
        #Take all partial traj and shuffle their order!
        n_traj = all_traj.n_traj

        active_workers = workers()
        n_workers = length(active_workers)

        # Create Channels storing results and starting values
        input_channel  = RemoteChannel(()->Channel{Trajectory}(length(workers())+1)) # will contain networks and traj
        output_channel = RemoteChannel(()->Channel{Trajectory}(length(workers())+1)) # will contain networks

        # Create problem on every worker
        begin
            for p in workers()
                remote_do(distributed_gae, p, params, input_channel, output_channel, θv)
            end
        end

        # Start optimizations
        n_parallel = Int(ceil(n_traj / n_workers))
        prog       = Progress(n_traj,1)

        # Start optimizations
        for i_traj = 1:n_traj
                traj_local = all_traj.traj[i_traj]
                @async put!(input_channel, traj_local)
        end

        for i_traj = 1:n_traj
                #Take from Channel:
                all_traj.traj[i_traj] = copy_traj(take!(output_channel),params)
        end

        # Tell workers that its done
        NaN_mat = zeros(length(params.batch_size))
        fill!(NaN_mat,NaN)
        ending_traj = init_traj(params,params.batch_size)
        ending_traj.r_t = NaN_mat

        for j = 1 : n_workers
                @async put!(input_channel, ending_traj)
        end

        #Close Channels
        sleep(0.1)
        close(input_channel)
        close(output_channel)
end

#Function running on the worker
@everywhere function distributed_gae(params::Params, input_channel::RemoteChannel, output_channel::RemoteChannel, θv)

   traj_local = init_traj(params,params.l_traj)
   # Start loop where work is done
   while true
       # break loop if NaN state received
       try
           #Take the new Input
           traj_local = copy_traj(take!(input_channel),params)

           #Check if u reached final
           if (sum(isnan, traj_local.r_t) > 0)
                     break
           end

           #DO THE WORK:
           #-----------------------------------------------------------------------
           #Recalculate the values for the states
           generate_values!(traj_local,params,θv)
           #recalculate the gae
           gae!(params,traj_local)
           #-----------------------------------------------------------------------

           put!(output_channel,copy_traj(traj_local,params))

       catch
           println(traj_local)
           println("Some problem occured, worker $(myid()) exiting trajectory optimization function.")
           break
       end
   end

   return nothing
end

#Function calculating the values for each state in a trajectory
@everywhere function generate_values!(traj::Trajectory,params::Params,θv::Any)
    for i_t = 1:traj.len
        s_t = traj.s_t[i_t,:]

        #Only for Segway
        s_mod = s_t
        #=---------------------------------
        s_mod        = zeros(8)
        s_mod[1:2]   = s_t[1:2]
        s_mod[3]     = sin(s_t[3])
        s_mod[4]     = cos(s_t[3])
        s_mod[5:end] = s_t[4:end]
        #-------------------------------=#

        val_t = mlp_nlayer_value_norm(θv,s_mod,params)[1]
        traj.val_t[i_t] = val_t
    end
end


#=
This file is auto-generated. Do not edit.
=#

#! format: off

"""
    mutable struct AVRSimple <: AVR
        Kv::Float64
        V_ref::Float64
        ext::Dict{String, Any}
        states::Vector{Symbol}
        n_states::Int
        states_types::Vector{StateTypes}
        internal::InfrastructureSystemsInternal
    end

Parameters of a simple proportional AVR in the derivative of EMF
i.e. an integrator controller on EMF

# Arguments
- `Kv::Float64`: Proportional Gain, validation range: `(0, nothing)`
- `V_ref::Float64`: Reference Voltage Set-point, validation range: `(0, nothing)`
- `ext::Dict{String, Any}`
- `states::Vector{Symbol}`: The states are:
	Vf: field voltage
- `n_states::Int`: Fixed AVR has 1 states
- `states_types::Vector{StateTypes}`: Simple AVR has 1 differential states
- `internal::InfrastructureSystemsInternal`: power system internal reference, do not modify
"""
mutable struct AVRSimple <: AVR
    "Proportional Gain"
    Kv::Float64
    "Reference Voltage Set-point"
    V_ref::Float64
    ext::Dict{String, Any}
    "The states are:
	Vf: field voltage"
    states::Vector{Symbol}
    "Fixed AVR has 1 states"
    n_states::Int
    "Simple AVR has 1 differential states"
    states_types::Vector{StateTypes}
    "power system internal reference, do not modify"
    internal::InfrastructureSystemsInternal
end

function AVRSimple(Kv, V_ref=1.0, ext=Dict{String, Any}(), )
    AVRSimple(Kv, V_ref, ext, [:Vf], 1, [StateTypes.Differential], InfrastructureSystemsInternal(), )
end

function AVRSimple(; Kv, V_ref=1.0, ext=Dict{String, Any}(), states=[:Vf], n_states=1, states_types=[StateTypes.Differential], internal=InfrastructureSystemsInternal(), )
    AVRSimple(Kv, V_ref, ext, states, n_states, states_types, internal, )
end

# Constructor for demo purposes; non-functional.
function AVRSimple(::Nothing)
    AVRSimple(;
        Kv=0,
        V_ref=0,
        ext=Dict{String, Any}(),
    )
end

"""Get [`AVRSimple`](@ref) `Kv`."""
get_Kv(value::AVRSimple) = value.Kv
"""Get [`AVRSimple`](@ref) `V_ref`."""
get_V_ref(value::AVRSimple) = value.V_ref
"""Get [`AVRSimple`](@ref) `ext`."""
get_ext(value::AVRSimple) = value.ext
"""Get [`AVRSimple`](@ref) `states`."""
get_states(value::AVRSimple) = value.states
"""Get [`AVRSimple`](@ref) `n_states`."""
get_n_states(value::AVRSimple) = value.n_states
"""Get [`AVRSimple`](@ref) `states_types`."""
get_states_types(value::AVRSimple) = value.states_types
"""Get [`AVRSimple`](@ref) `internal`."""
get_internal(value::AVRSimple) = value.internal

"""Set [`AVRSimple`](@ref) `Kv`."""
set_Kv!(value::AVRSimple, val) = value.Kv = val
"""Set [`AVRSimple`](@ref) `V_ref`."""
set_V_ref!(value::AVRSimple, val) = value.V_ref = val
"""Set [`AVRSimple`](@ref) `ext`."""
set_ext!(value::AVRSimple, val) = value.ext = val
"""Set [`AVRSimple`](@ref) `states_types`."""
set_states_types!(value::AVRSimple, val) = value.states_types = val


"""
    _group_by_friends!(ata_model::AbstractModel)

# Description

Group the items by friend sets once the friend sets have been added to the `ATA.AbstractModel` by [`add_friends!`](#ATA.add_friends!-Tuple{ATA.AbstractModel}) function and as **last operation** of the assembly.

# Arguments

- **`ata_model::AbstractModel`** : Required. The model built with `start_ata()`, settings loaded by [`start_ata`](#ATA.start_ata) function and friend sets added by [`add_friends!`](#ATA.add_friends!-Tuple{ATA.AbstractModel}) function.
"""
function _group_by_friends!(ata_model::AbstractModel) #last
    try
        n_items = ata_model.settings.n_items
        #only works for categorical variables and item use, all the other contraitns need expansion by fs_items
        if ata_model.settings.n_fs == 0
            error!(ata_model, "No friend sets found, run start_ata() before.")
            return nothing
        end
        if any([
            size(ata_model.constraints[t].constr_b, 1) > 0 for t = 1:ata_model.settings.T
        ])
            A = Vector{Matrix{Float64}}(undef, ata_model.settings.T)
            b = Vector{Vector{Float64}}(undef, ata_model.settings.T)
            A_new = Vector{Matrix{Float64}}(undef, ata_model.settings.T)
            for t = 1:ata_model.settings.T
                A[t] = ata_model.constraints[t].constr_A
                b[t] = ata_model.constraints[t].constr_b
            end
            n_fs = ata_model.settings.n_fs
            for t = 1:ata_model.settings.T
                A_new[t] = Matrix{Float64}(undef, size(A[t], 1), n_fs)
            end
            for fs = 1:n_fs
                for t = 1:ata_model.settings.T
                    A_new[t][:, fs] =
                        sum(A[t][:, ata_model.settings.fs.items[fs]], dims = 2)
                end
            end
            for t = 1:ata_model.settings.T
                DelimitedFiles.writedlm("opt/A_$t.csv", A_new[t])
                DelimitedFiles.writedlm("opt/b_$t.csv", b[t])
                ata_model.constraints[t].constr_A = A_new[t]
            end
            open("opt/fs_items.jl", "w") do f
                write(f, "fs_items = Vector{Vector{Int64}}(undef, $n_fs)\n")
                for fs = 1:n_fs
                    write(
                        f,
                        string("fs_items[$fs] =", ata_model.settings.fs.items[fs], "\n"),
                    )
                end
            end
            # open("opt/Settings.jl", "a") do f
            #     write(f, "group_by_fs = true\n\n")
            # end
            #transform forced0
            x_forced0 = ata_model.settings.forced0
            x_forced0_new = Vector{Vector{Bool}}(undef, ata_model.settings.T)
            for v = 1:ata_model.settings.T
                x_forced0_new[v] = fill(true, n_fs)
                for i = 1:ata_model.settings.n_items
                    if x_forced0[v][i] == false
                        for fs = 1:n_fs
                            if any(i .== ata_model.settings.fs.items[fs])
                                x_forced0_new[v][fs] = false
                            end
                        end
                    end
                end
            end
            #update ata_model.forced0
            ata_model.settings.forced0 = x_forced0_new
            DelimitedFiles.writedlm("opt/x_forced0.txt", x_forced0_new)
            success!(
                ata_model,
                "Categorical and linear quantitative constraints grouped by friend sets.",
            )
        end
        #item use
        if ata_model.settings.to_apply[1]
            item_use_max = ata_model.settings.iu.max
            item_use_max_new = zeros(Int, n_fs)
            for fs = 1:n_fs
                item_use_max_new[fs] =
                    Int(minimum(ata_model.settings.iu.max[ata_model.settings.fs.items[fs]]))
            end
            ata_model.settings.iu.max = item_use_max_new
            open("opt/Settings.jl", "a") do f
                write(f, "item_use_max = $item_use_max_new\n\n")
            end
            success!(ata_model, "Maximum item use grouped.")
        end
        if ata_model.settings.to_apply[2]
            item_use_min = ata_model.settings.iu.min
            item_use_min_new = zeros(Int, n_fs)
            for fs = 1:n_fs
                item_use_min_new[fs] =
                    Int(maximum(ata_model.settings.iu.min[ata_model.settings.fs.items[fs]]))
            end
            ata_model.settings.iu.min = item_use_min_new
            open("opt/Settings.jl", "a") do f
                write(f, "item_use_min = $item_use_min_new\n\n")
            end
            success!(ata_model, "Minimum item use grouped.")
        end
    catch e
        error!(ata_model, string("", sprint(showerror, e)))
    end
    return nothing
end


import Base.Git
include("gitutils.jl")

dir = string("tmp.",randstring())
@test !ispath(dir)
mkdir(dir)
@test isdir(dir)
try cd(dir) do

    run(`git init -q`)
    run(`git commit -q --allow-empty -m "initial empty commit"`)
    git_verify(Dict(), Dict(), Dict())

    # each path can have one of these content in each of head, index, work
    # for a total of length(contents)^3 = 4^3 = 64 combinations.
    # each path can be in any of these 64 "superpositions" before & after
    # for a total of 64^2 = 4096 files needed to test all transitions
    # between before and after superpositions of git repo states.

    contents = [nothing, "foo", "bar", {"baz"=>"qux"}]
    b = length(contents)
    states = [ [ base(b,k,6) => contents[rem(div(k,b^p),b)+1] for k=0:(b^3)^2-1 ] for p=0:5 ]

    git_setup(states[1:3]...)
    try Git.transact() do
        git_setup(states[4:6]...)
        throw(nothing)
    end catch x
        is(x,nothing) || rethrow()
    end
    git_verify(states[1:3]...)

end finally run(`rm -rf $dir`) end


using BERT
using Knet
import Base: length, iterate
using Random
using CSV
using PyCall
using Dates

VOCABFILE = "bert-base-uncased-vocab.txt"
NUM_CLASSES = 2
LEARNING_RATE = 2e-5
NUM_OF_EPOCHS = 30
TRAIN = true

token2int = Dict()
f = open(VOCABFILE) do file
    lines = readlines(file)
    for (i,line) in enumerate(lines)
        token2int[line] = i
    end
end
int2token = Dict(value => key for (key, value) in token2int)
VOCABSIZE = length(token2int)

# include("preprocess.jl")
# include("optimizer.jl")

function convert_to_int_array(text, dict; lower_case=true)
    tokens = bert_tokenize(text, dict, lower_case=lower_case)
    out = Int[]
    for token in tokens
        if token in keys(dict)
            push!(out, dict[token])
        else
            push!(out, dict["[UNK]"])
        end
    end
    return out
end

function read_and_process(filename, dict; lower_case=true)
    data = CSV.File(filename, delim="\t")
    x = Array{Int,1}[]
    y = Int8[]
    for i in data
        push!(x, convert_to_int_array(i.sentence, dict, lower_case=lower_case))
        push!(y, Int8(i.label + 1)) # negative 1, positive 2
    end

    # Padding to maximum
#     max_seq = findmax(length.(x))[1]
#     for i in 1:length(x)
#         append!(x[i], fill(1, max_seq - length(x[i]))) # 1 is for "[PAD]"
#     end

    return (x, y)
end

mutable struct ClassificationData
    input_ids
    input_mask
    segment_ids
    labels
    batchsize
    ninstances
    shuffled
end

function ClassificationData(input_file, token2int; batchsize=8, shuffled=true, seq_len=64)
    input_ids = []
    input_mask = []
    segment_ids = []
    labels = []
    (x, labels) = read_and_process(input_file, token2int)
    for i in 1:length(x)
        if length(x[i]) >= seq_len
            x[i] = x[i][1:seq_len]
            mask = Array{Int64}(ones(seq_len))
        else
            mask = Array{Int64}(ones(length(x[i])))
            append!(x[i], fill(1, seq_len - length(x[i]))) # 1 is for "[PAD]"
            append!(mask, fill(0, seq_len - length(mask))) # 0's vanish with masking operation
        end
        push!(input_ids, x[i])
        push!(input_mask, mask)
        push!(segment_ids, Array{Int64}(ones(seq_len)))
    end
    ninstances = length(input_ids)
    return ClassificationData(input_ids, input_mask, segment_ids, labels, batchsize, ninstances, shuffled)
end

function length(d::ClassificationData)
    d, r = divrem(d.ninstances, d.batchsize)
    return r == 0 ? d : d+1
end

function iterate(d::ClassificationData, state=ifelse(d.shuffled, randperm(d.ninstances), 1:d.ninstances))

    state === nothing && return nothing

    if length(state) > d.batchsize
        new_state = state[d.batchsize+1:end]
        input_ids = hcat(d.input_ids[state[1:d.batchsize]]...)
        input_mask = hcat(d.input_mask[state[1:d.batchsize]]...)
        segment_ids = hcat(d.segment_ids[state[1:d.batchsize]]...)
        labels = hcat(d.labels[state[1:d.batchsize]]...)
    else
        new_state = nothing
        input_ids = hcat(d.input_ids[state]...)
        input_mask = hcat(d.input_mask[state]...)
        segment_ids = hcat(d.segment_ids[state]...)
        labels = hcat(d.labels[state]...)
    end

    return ((input_ids, input_mask, segment_ids, labels), new_state)
end

# mutable struct ClassificationData2
#     input_ids
#     input_mask
#     segment_ids
#     labels
#     batchsize
#     ninstances
#     shuffled
# end

# function ClassificationData2(input_file; batchsize=8, shuffled=true, seq_len=64)
#     input_ids = []
#     input_mask = []
#     segment_ids = []
#     labels = []
#     f = open(input_file)
#     tmp = split.(readlines(f), "\t")
#     for i in 1:length(tmp)
#         instance = eval.(Meta.parse.(tmp[i]))
#         push!(input_ids, (instance[1] .+ 1)[1:seq_len])
#         push!(input_mask, instance[2][1:seq_len])
#         push!(segment_ids, (instance[3] .+ 1)[1:seq_len])
#         push!(labels, (instance[4] + 1))
#     end
#     ninstances = length(input_ids)
#     return ClassificationData2(input_ids, input_mask, segment_ids, labels, batchsize, ninstances, shuffled)
# end

# function length(d::ClassificationData2)
#     d, r = divrem(d.ninstances, d.batchsize)
#     return r == 0 ? d : d+1
# end

# function iterate(d::ClassificationData2, state=ifelse(d.shuffled, randperm(d.ninstances), 1:d.ninstances))

#     state === nothing && return nothing

#     if length(state) > d.batchsize
#         new_state = state[d.batchsize+1:end]
#         input_ids = hcat(d.input_ids[state[1:d.batchsize]]...)
#         input_mask = hcat(d.input_mask[state[1:d.batchsize]]...)
#         segment_ids = hcat(d.segment_ids[state[1:d.batchsize]]...)
#         labels = hcat(d.labels[state[1:d.batchsize]]...)
#     else
#         new_state = nothing
#         input_ids = hcat(d.input_ids[state]...)
#         input_mask = hcat(d.input_mask[state]...)
#         segment_ids = hcat(d.segment_ids[state]...)
#         labels = hcat(d.labels[state]...)
#     end

#     return ((input_ids, input_mask, segment_ids, labels), new_state)
# end

# include("model.jl")

# Embedding Size, Vocab Size, Intermediate Hidden Size, Max Sequence Length, Sequence Length, Num of Segments, Num of Heads in Attention, Num of Encoders in Stack, Batch Size, Matrix Type, General Dropout Rate, Attention Dropout Rate, Activation Function
config = BertConfig(768, 30522, 3072, 512, 64, 2, 12, 12, 8, KnetArray{Float32}, 0.1, 0.1, "gelu")

if TRAIN
    dtrn = ClassificationData("../project/mytrain.tsv", token2int, batchsize=config.batchsize, seq_len=config.seq_len)
    ddev = ClassificationData("../project/dev.tsv", token2int, batchsize=config.batchsize, seq_len=config.seq_len)
else
    dtst = ClassificationData("../project/mytest.tsv", token2int, batchsize=config.batchsize, seq_len=config.seq_len)
end

if TRAIN
    model = BertClassification(config, NUM_CLASSES)

    @pyimport torch
    torch_model = torch.load("../project/pytorch_model.bin")

    model = load_from_torch_base(model, config.num_encoder, config.atype, torch_model)
end

function accuracy2(model, dtst)
    true_count = 0
    all_count = 0
    for (x, attention_mask, segment_ids, y) in dtst
        probs = model(x, segment_ids, attention_mask=attention_mask)
        preds = map(x -> x[1], argmax(Array{Float32}(probs),dims=1))
        true_count += sum(y .== preds)
        all_count += length(y)
    end
    return true_count/all_count
end

function initopt!(model, t_total; lr=0.001, warmup=0.1)
    for par in params(model)
        if length(size(value(par))) === 1
            par.opt = BertAdam(lr=lr, warmup=warmup, t_total=t_total, w_decay_rate=0.01)
        else
            par.opt = BertAdam(lr=lr, warmup=warmup, t_total=t_total)
        end
    end
end

function mytrain!(model, dtrn, ddev, best_acc)
    losses = []
    accs = []
    for (k, (x, attention_mask, segment_ids, labels)) in enumerate(dtrn)
        J = @diff model(x, segment_ids, labels, attention_mask=attention_mask)
        for par in params(model)
            g = grad(J, par)
            update!(value(par), g, par.opt)
        end
        push!(losses, value(J))
        if k % 500 == 0
            print(Dates.format(now(), "HH:MM:SS"), "  ->  ")
            println("Training loss up to $k iteration is : ", Knet.mean(losses))
            flush(stdout)
            acc = accuracy2(model, ddev)
            push!(accs, acc)
            print(Dates.format(now(), "HH:MM:SS"), "  ->  ")
            println("Accuracy at $k iteration : ", acc)
            flush(stdout)
            if acc > best_acc
                best_acc = acc
                print(Dates.format(now(), "HH:MM:SS"), "  ->  ")
                println("Saving...")
                Knet.save("model_bert.jld2", "model", model)
                flush(stdout)
            end
        end
    end
    return (best_acc, Knet.mean(losses), accs)
end

if TRAIN
    t_total = length(dtrn) * NUM_OF_EPOCHS
    initopt!(model, t_total, lr=LEARNING_RATE)

    dev_accs = [0.0]
    best_acc = 0.0
    for epoch in 1:NUM_OF_EPOCHS
        global best_acc
        print(Dates.format(now(), "HH:MM:SS"), "  ->  ")
        println("Epoch : ", epoch)
        flush(stdout)
        (best_acc, lss, acc) = mytrain!(model, dtrn, ddev, best_acc)
        append!(dev_accs, acc)
        print(Dates.format(now(), "HH:MM:SS"), "  ->  ")
        println("Training loss for $epoch epoch is : $lss")
        println(dev_accs)
        flush(stdout)
        #=
        acc = accuracy2(model, ddev)
        println("Accuracy : ", acc)
        if acc > best_acc
            best_acc = acc
            println("Saving...")
            Knet.save("model_bert.jld2", "model", model)
        end
        =#
    end

    Knet.save("accuracies.jld2", "dev_accs", dev_accs)
else
    model = Knet.load("model_bert.jld2", "model")
    result = accuracy2(model, dtst)
    print(Dates.format(now(), "HH:MM:SS"), "  ->  ")
    println("Test accuracy is : $result")
end