Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Make docs chapter on default units and physical constants #704

Open
wants to merge 14 commits into
base: master
Choose a base branch
from
2 changes: 2 additions & 0 deletions docs/Project.toml
Original file line number Diff line number Diff line change
Expand Up @@ -4,7 +4,9 @@ Documenter = "e30172f5-a6a5-5a46-863b-614d45cd2de4"
InverseFunctions = "3587e190-3f89-42d0-90ee-14403ec27112"
Latexify = "23fbe1c1-3f47-55db-b15f-69d7ec21a316"
Markdown = "d6f4376e-aef5-505a-96c1-9c027394607a"
OrderedCollections = "bac558e1-5e72-5ebc-8fee-abe8a469f55d"

[compat]
Documenter = "1"
Latexify = "0.16"
OrderedCollections = "1.6"
30 changes: 27 additions & 3 deletions docs/make.jl
Original file line number Diff line number Diff line change
@@ -1,11 +1,31 @@
using Documenter, Unitful, Dates

const ci = get(ENV, "CI", nothing) == "true"

function check_defaultunits_version()
vfile = "docs/src/assets/vfile.txt"
r = readline(vfile)
docs_v = VersionNumber(r)
pkg_v = pkgversion(Unitful)
docs_v == pkg_v || error("Docs chapter on default units built with the wrong version of Unitful \
(docs built for $docs_v vs current Unitful version $pkg_v). \
Please run the script on the local computer with the proper Unitful version")
return nothing
end

# on local computer, (re-)create the documentation file defaultunits.md
if !ci
ENV["UNITFUL_FANCY_EXPONENTS"] = false
include("make_def-units_docs.jl")
MakeDefUnitsDocs.make_chapter()
end

DocMeta.setdocmeta!(Unitful, :DocTestSetup, :(using Unitful))

makedocs(
sitename = "Unitful.jl",
format = Documenter.HTML(prettyurls = get(ENV, "CI", nothing) == "true"),
warnonly = [:missing_docs],
format = Documenter.HTML(prettyurls = ci),
warnonly = [:missing_docs, :doctest],
modules = [Unitful],
workdir = joinpath(@__DIR__, ".."),
pages = [
Expand All @@ -21,8 +41,12 @@ makedocs(
"Interoperability with `Dates`" => "dates.md"
"Extending Unitful" => "extending.md"
"Troubleshooting" => "trouble.md"
"Pre-defined units and constants" => "defaultunits.md"
"License" => "LICENSE.md"
]
)

deploydocs(repo = "github.com/PainterQubits/Unitful.jl.git")
if ci
check_defaultunits_version()
deploydocs(repo = "github.com/PainterQubits/Unitful.jl.git")
end
292 changes: 292 additions & 0 deletions docs/make_def-units_docs.jl
Original file line number Diff line number Diff line change
@@ -0,0 +1,292 @@
module MakeDefUnitsDocs

using Unitful, OrderedCollections
sostock marked this conversation as resolved.
Show resolved Hide resolved

mdfile = "docs/src/defaultunits.md"
mdheader = "docs/src/assets/defaultunits-header.md"
mdfooter = "docs/src/assets/defaultunits-footer.md"
mdlogunits = "docs/src/assets/defaultunits-logunits.md"
vfile = "docs/src/assets/vfile.txt"

"""
# Examples
```julia-repl
julia> prefnamesvals()
OrderedCollections.OrderedDict{String, Tuple{String, Int64}} with 20 entries:
"y" => ("yocto", -24)
"z" => ("zepto", -21)
⋮ => ⋮
"""
function prefnamesvals()
prefixnames = Dict(
"Q" => "quetta",
"R" => "ronna",
"Y" => "yotta",
"Z" => "zetta",
"E" => "exa",
"P" => "peta",
"T" => "tera",
"G" => "giga",
"M" => "mega",
"k" => "kilo",
"h" => "hecto",
"da" => "deca",
"d" => "deci",
"c" => "centi",
"m" => "milli",
"μ" => "micro",
"n" => "nano",
"p" => "pico",
"f" => "femto",
"a" => "atto",
"z" => "zepto",
"y" => "yocto",
"r" => "ronto",
"q" => "quecto")
pd = Unitful.prefixdict
sxp = sort(collect(keys(pd)))

return OrderedDict(pd[k] => (prefixnames[pd[k]], k) for k in sxp if pd[k] != "")
end

regularid(n) = !startswith(string(n), r"#|@")

uful_ids() = filter(regularid, names(Unitful; all=true))

docstr(n::Symbol) = Base.Docs.doc(Base.Docs.Binding(Unitful, n)) |> string

isprefixed(u::Symbol) = occursin("A prefixed unit, equal", docstr(u))
Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Looking at the docstring to determine whether a unit is prefixed doesn’t seem like the best solution to me. For example, it doesn’t recognise ha as a prefixed unit because it has a custom docstring. Maybe it is sufficient for now to add ha as an exception.


isdocumented(n::Symbol) = !startswith(docstr(n), "No documentation found.")

"""
getphysdims(uids::Vector{Symbol})
Filters the list of `Unitful` identifiers to return those which denote physical dimensions (e.g. `Area`, `Power`)
"""
getphysdims(uids) = filter(isphysdim, uids)

isphysdim(n::Symbol) = _isphysdim(getproperty(Unitful, n))
_isphysdim(_) = false
_isphysdim(::Type{Union{Quantity{T,D,U}, Level{L,S,Quantity{T,D,U}} where {L,S}} where {T,U}}) where D = true

"""
# Examples
```julia-repl
julia> getdim(Unitful.Area)
𝐋^2
```
"""
getdim(::Type{Union{Quantity{T,D,U}, Level{L,S,Quantity{T,D,U}} where {L,S}} where {T,U}}) where D = D
getdim(x::Symbol) = getdim(getproperty(Unitful, x))

"""
# Examples
```julia-repl
julia> getdimpars(Unitful.Power)
svec((Unitful.Dimension{:Length}(2//1), Unitful.Dimension{:Mass}(1//1), Unitful.Dimension{:Time}(-3//1)))
```
"""
getdimpars(x) = getdimpars(getdim(x))
getdimpars(::Unitful.Dimensions{N}) where N = N

getdimpow(x) = only(getdimpars(x)).power

isbasicdim(x) = length(getdimpars(x)) == 1 && getdimpow(x) == 1

function physdims_categories(physdims)
basicdims = Symbol[]
compounddims = Symbol[]
otherdims = Symbol[]
for d in physdims
try
if isbasicdim(d)
push!(basicdims, d)
else
push!(compounddims, d)
end
catch
push!(otherdims, d)
end
end
return (;basicdims, compounddims, otherdims, )
end

"""
# Examples
```julia-repl
julia> unitsdict(basicdims, uids)
OrderedCollections.OrderedDict{Symbol, Vector{Symbol}} with 7 entries:
:Amount => [:mol]
:Current => [:A]
:Length => [:angstrom, :ft, :inch, :m, :mi, :mil, :yd]
:Luminosity => [:cd, :lm]
:Mass => [:dr, :g, :gr, :kg, :lb, :oz, :slug, :u]
:Temperature => [:K, :Ra, :°C, :°F]
:Time => [:d, :hr, :minute, :s, :wk, :yr]
```
"""
function unitsdict(physdims, uids)
ups = []
for d in physdims
dm = getproperty(Unitful, d)
units = Symbol[]
for uname in uids
u = getproperty(Unitful, uname)
if (u isa Unitful.Units)
if (1*u isa dm) && (!isprefixed(uname) || uname == :g) && isdocumented(uname) # gram considered prefixed unit
push!(units, uname)
end
end
end
if !isempty(units)
sort!(units; by = x -> lowercase(string(x)))
unique!(nameofunit, units) # special cases: Liter, Angstrom
push!(ups, d => units)
end
end
return OrderedDict(sort!(ups))
end

function physconstants(uids)
ph_consts = [n for n in uids if
isconst(Unitful, n) &&
!(getproperty(Unitful, n) isa Union{Type, Unitful.Units, Unitful.Dimensions, Module, Function}) &&
isdocumented(n) ]
sort!(ph_consts)
return ph_consts
end

function isnodims(u)
u isa Unitful.FreeUnits || return false
return dimension(u) == NoDims
end
isnodims(u::Symbol) = isnodims(getproperty(Unitful, u))

nodimsunits(uids) = [n for n in uids if isnodims(n) && isdocumented(n) && !isprefixed(n) && n != :NoUnits]

removerefs(d) = replace(d, r"\[(`[\w\.]+\`)]\(@ref\)" => s"\1")

"""
udoc(s)
Truncates documentation and removes references
"""
udoc(s) = match(r"(?ms)(.+)\n\nDimension: ", docstr(s)).captures[1] |> removerefs

function nameofunit(u)
special = Dict(u"ha" => "Hectare", u"kg" => "Kilogram", u"°F" => "Degree Fahrenheit", u"°C" => "Degree Celcius")
u in keys(special) && return special[u]
return string(_nameofunit(u))
end

nameofunit(s::Symbol) = nameofunit(getproperty(Unitful, s))

_nameofunit(::Unitful.Units{N}) where N = _nameofunit(only(N))
_nameofunit(::Unitful.Unit{U}) where U = U

function make_subsection_text(uvec; isunit=true)
s = ""
for u in uvec
if isunit
n = nameofunit(u)
else
n = string(u)
end
d = udoc(u)
s *= "#### $n\n\n$d\n\n"
end
return s
end

function make_simple_section_text(sectiontitle, uvec; isunit=true)
s = "## $sectiontitle\n\n"
s *= make_subsection_text(uvec; isunit)
return s
end

function make_structured_section_text(sectiontitle, sectiondict)
s = "## $sectiontitle\n\n"
for (dim, uvec) in sectiondict
s *= "### $dim\n\n"
s *= make_subsection_text(uvec)
end
return s
end

function makeprefixsection(pnv)
s = """
## Metric (SI) Prefixes

| Prefix | Name | Power of Ten |
|--------|--------|--------|
"""
for (k,v) in pnv
s *= "| $k | $(v[1]) | $(v[2]) |\n"
end

return s
end


header() = read(mdheader, String)
footer() = read(mdfooter, String)
logunits() = read(mdlogunits, String)

function makefulltext(sections, nodims_units, phys_consts)
s = header() * "\n\n"
for (sectiontitle, sectiondict) in sections
s *= make_structured_section_text(sectiontitle, sectiondict)
end
s *= make_simple_section_text("Dimensionless units", nodims_units)
s *= logunits()
s *= make_simple_section_text("Physical constants", phys_consts; isunit=false)
s *= makeprefixsection(prefnamesvals())
s *= footer()
return s
end

function write_unitful_v(vfile)
open(vfile, "w") do io
println(io, pkgversion(Unitful))
end
return nothing
end

function savetext(fulltext, mdfile)
open(mdfile,"w") do io
write(io, fulltext)
end
write_unitful_v(vfile)
return nothing
end

"""
make_chapter(wr = true; verbose = false)
Generates the text of the `Pre-defined units and constants` documentation section
and writes it into the file if `wr==true`
"""
function make_chapter(wr = true; verbose = false)
uids = uful_ids()

(;basicdims, compounddims) = uids |> getphysdims |> physdims_categories

basic_units = unitsdict(basicdims, uids)
compound_units = unitsdict(compounddims, uids)
Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

If I understand correctly, this only collects units of dimensions that were declared with @derived_dimension. If we add units whose dimension isn’t “named”, they won’t show up here.

nodims_units = nodimsunits(uids)
sections = OrderedDict(["Base dimensions" => basic_units,
"Derived dimensions" => compound_units])
phys_consts = physconstants(uids)

fulltext = makefulltext(sections, nodims_units, phys_consts)

wr && savetext(fulltext, mdfile)

if verbose
return (;fulltext, sections, nodims_units, phys_consts)
else
return nothing
end
end

export make_chapter

end # module
Empty file.
16 changes: 16 additions & 0 deletions docs/src/assets/defaultunits-header.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,16 @@
# Pre-defined units and сonstants

In the following, only non-prefixed units are listed. To get a more detailed information about a unit, and to get information about prefixed units, use `Julia` help, e.g.

```
help?> Unitful.kW
Unitful.kW

A prefixed unit, equal to 10^3 W.

Dimension: 𝐋² 𝐌 𝐓⁻³

See also: Unitful.W.
```

For prefixes, see [below](#Metric-(SI)-Prefixes).
21 changes: 21 additions & 0 deletions docs/src/assets/defaultunits-logunits.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,21 @@
## Logarithmic units

| Unit | Name |
|----------------|---------------------------------|
| `dB` | Decibel |
| `B` | Bel |
| `Np` | Neper |
| `cNp` | Centineper |

### "Dimensionful" logarithmic units
Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I wouldn’t call this “dimensionful” (dBFS is dimensionless). Maybe “Logarithmic units relative to reference levels”?

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Then maybe “Log units related to reference levels” to keep the title a bit shorter (and probably related, not relative - but I am never sure with my English)

| Unit | Reference level |
|----------------|---------------------------------|
| `dBHz` | 1Hz |
| `dBm` | 1mW |
| `dBV` | 1V |
| `dBu` | sqrt(0.6)V |
| `dBμV` | 1μV |
| `dBSPL` | 20μPa |
| `dBFS` | RootPowerRatio(1) |
| `dBΩ` | 1Ω |
| `dBS` | 1S |
1 change: 1 addition & 0 deletions docs/src/assets/vfile.txt
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
1.19.0
Loading
Loading