Skip to content

Commit

Permalink
More documentation improvements (#2)
Browse files Browse the repository at this point in the history
  • Loading branch information
timholy authored Oct 9, 2023
1 parent 2b6bf49 commit 64f17fb
Show file tree
Hide file tree
Showing 7 changed files with 191 additions and 37 deletions.
4 changes: 4 additions & 0 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -3,3 +3,7 @@
[![Dev](https://img.shields.io/badge/docs-dev-blue.svg)](https://timholy.github.io/ThickNumbers.jl/dev/)
[![Build Status](https://github.com/timholy/ThickNumbers.jl/actions/workflows/CI.yml/badge.svg?branch=main)](https://github.com/timholy/ThickNumbers.jl/actions/workflows/CI.yml?query=branch%3Amain)
[![Coverage](https://codecov.io/gh/timholy/ThickNumbers.jl/branch/main/graph/badge.svg)](https://codecov.io/gh/timholy/ThickNumbers.jl)

This package defines a new abstract type, `ThickNumber{T}`, which is like Julia's `Number` but represents numbers that may have a "thickness" or "width". Such numbers generally support arithmetic but also act like sets, in the sense that they contain spans of "point" numbers. Prominent examples of such numbers include [intervals](https://en.wikipedia.org/wiki/Interval_arithmetic) and [gaussian random variables](https://en.wikipedia.org/wiki/Algebra_of_random_variables).

It also defines a common API for working with ThickNumber types, making it possible to write code that supports multiple `ThickNumber` subtypes simultaneously. See the [documentation](https://timholy.github.io/ThickNumbers.jl/dev/) for details.
2 changes: 2 additions & 0 deletions docs/make.jl
Original file line number Diff line number Diff line change
Expand Up @@ -11,11 +11,13 @@ makedocs(;
format=Documenter.HTML(;
prettyurls=get(ENV, "CI", "false") == "true",
canonical="https://timholy.github.io/ThickNumbers.jl",
repolink="https://github.com/timholy/ThickNumbers.jl",
edit_link="main",
assets=String[],
),
pages=[
"Home" => "index.md",
"developers.md",
"required.md",
"optional.md",
"user_api.md",
Expand Down
40 changes: 40 additions & 0 deletions docs/src/developers.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,40 @@
# Creating a new ThickNumber subtype

Create your type by subtyping `ThickNumber{T}`:

```julia
struct MyType{T<:Real} <: ThickNumber{T}
# you can have whatever fields you need...
a::T
b::T
end
```

If you only intend to support, say, `Float64`, you can use `struct MyType <: ThickNumber{Float64} ... end`: the key point is that the `T` in `ThickNumber{T}` should encode the [`valuetype`](@ref).

The following interface functions must be defined:

- [`loval(x)`](@ref): should return the lower span (i.e., the "lower bound" if such bounds are strict).
- [`hival(x)`](@ref): should return the upper span.
- any arithmetic operations you need, e.g., `+`, `-`, `*`, and `/`

The implementation of these functions must satisfy certain requirements spelled out in the documentation of each of these functions.

If possible, you should also define:

- [`lohi(MyType{T}, lo, hi)`](@ref): construct `x` from two numbers specifying the lower and upper spans.

If you cannot define this for your type (e.g., `MyType` requires more than two arguments to construct), it is likely that you'll have to specialize some of the [User API](@ref) functions for `MyType`, since the default implementations of some of them rely on `lohi`.

There are also numerous optional methods you can specialize if it makes `MyType` operate more
efficiently. For example, a gaussian random variable package might want to implement [`midrad(MyType{T}, center, σ)`](@ref) to construct values directly, assuming this is the natural parametrization
of this type.

## Ensuring compliance with the ThickNumbers interface

The `ThickNumbersInterfaceTests` package can be used to determine whether your implementations comply with the requirements. As it is possible that this test suite will evolve and add new requirements,
be sure to use `[compat]` bounds to specify the major version number of `ThickNumbersInterfaceTests` that your implementation satisfies.

## Features provided by subtyping ThickNumber

See the [User API](@ref).
82 changes: 57 additions & 25 deletions docs/src/index.md
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,13 @@ CurrentModule = ThickNumbers
A `ThickNumber{T}` is an abstract type denoting objects that act like numbers--they have standard algebraic operations `+`, `-`, `*`, and `/`--but also
have properties of a [connected set](https://en.wikipedia.org/wiki/Connected_space), specifically occupying some "width," e.g., a segment of the real number line. Examples of possible concrete subtypes include [intervals](https://en.wikipedia.org/wiki/Interval_arithmetic), [gaussian random variables](https://en.wikipedia.org/wiki/Algebra_of_random_variables), and potentially others. While the parameter `T` in `ThickNumber` does not necessarily have to be `T<:Real`, it should have an ordering so that "width" has some meaning.

This documentation is aimed at:

- *users* who what to know how to manipulate `ThickNumber` objects. Users should read:
+ [The Fundamental Principle of ThickNumbers (FPTN)](@ref), which explains key differences between `ThickNumber`s and "point" numbers
+ [The ThickNumber API](@ref), which lists the main functions used to manipulate ThickNumbers.
- *developers* who want to create a new `ThickNumber` subtype. Developers should read the two sections above followed by [Creating a new ThickNumber subtype](@ref), and then refer to the API reference sections as needed.

## The Fundamental Principle of ThickNumbers (FPTN)

An important issue that must be understood at the outset is a generalization of the
Expand All @@ -28,42 +35,67 @@ thus having `1..3 == 1..3` return `true` would be a violation of the FPTN; it mu

Because numbers are iterable in Julia, set operations like `X ⊆ Y` also cannot be defined (it would require that each number in `X` is a subset of every number in `Y`); however, operations like `intersect(X, Y)` (i.e., `X ∩ Y`) are valid because `x ∩ y` returns `` if `x != y` and `` is a subset of all other sets.

To avoid violating the FPTN, we replace operators like `==` with custom operators that work only on `ThickNumber{T}` but not `T`. For `Base` Julia functions, a convention is to add `_tn` after the standard function name:
To avoid violating the FPTN, we replace operators like `==` with custom operators that work only on `ThickNumber{T}` but not `T`. For `Base` Julia functions, a convention is to add `_tn` after the standard function name: `isequal_tn(X, Y)` replaces the "intent" of `isequal(X, Y)`. Often these have unicode equivalents, which typically (though not always) involve a "dot" somewhere in the symbol.

See the API section below for a more complete list of these replacements.

## The ThickNumber API

Let `x` and `y` refer to a standard "point" numbers and `X` and `Y` corresponding `ThickNumber`s such that `x ∈ X` and `y ∈ Y`.

### Querying values

With only a few exceptions, the names of these come from the Interval Arithmetic Standard (IEEE Std 1788-2015).

- [`loval(X)`](@ref): return the "lower bound" (which may not be "fuzzy" for some ThickNumber subtypes) of `X` (similar to `inf` in the IEEE standard, but without promising the true infimum)
- [`hival(X)`](@ref): return the "upper bound" of `X` (similar to `sup` in the IEEE standard)
- [`mid(X)`](@ref): return the midpoint of `X`
- [`wid(X)`](@ref): return the width (`hival - loval`) of `X`
- [`rad(X)`](@ref): return the half-width of `X` (half the value of `wid(X)`)
- [`mag(X)`](@ref): the largest absolute value contained in `X`
- [`mig(X)`](@ref): the smallest absolute value contained in `X`

You can also check a few basic properties, like whether the values contained in `X` are finite:

- [`isfinite_tn(X)`](@ref)
- [`isinf_tn(X)`](@ref)
- [`isnan_tn(X)`](@ref)

### Type information

- [`valuetype(X)`](@ref): return the type of numbers contained in `X` (e.g., `Float64`)

- `isequal_tn(X, Y)` replaces the "intent" of `X == Y` (unicode: ``)
- `isapprox_tn(X, Y)` replaces `` (unicode: ``)
- `≺(X, Y)` (`\prec`-TAB) replaces `x < y ∀ x ∈ X, y ∈ Y`; likewise, `` (`\succ`-TAB) replaces `>`
- `isless_tn(X, Y)` replaces `isless(x, y) ∀ x ∈ X, y ∈ Y`
- `issubset_tn` replaces `` (unicode: ``)
- `is_strict_subset_tn` replaces `` (unicode: ``)
- `issupset_tn` replaces `` (unicode: ``)
- `is_strict_supset_tn` replaces `` (unicode: ``)
### Generic constructors

## Creating a new ThickNumber subtype
Each `ThickNumber` subtype has its own constructor(s), but if you need a way to write generic code that works for multiple `ThickNumber` subtypes, you may be able to use:

To create a new type `MyType{T} <: ThickNumber{T}`, the following interface functions must be defined:
- [`lohi`](@ref): `lohi(TN, lo, hi)` creates a ThickNumber `X` where `typeof(X) <: TN`, `loval(X) ≈ lo`, and `hival(X) ≈ hi`. (It's approximate because of floating-point roundoff error and the fact that not all ThickNumber subtypes encode these bounds directly.)
- [`midrad`](@ref) creates a ThickNumber from its midpoint and radius (see [`mid`](@ref) and [`rad`](@ref)).

- [`lohi(MyType{T}, lo, hi)`](@ref): construct `x` from two numbers specifying the lower and upper spans. For example, for an interval `lo` and `hi` would be the left and right edges, respectively. (Conservative outward rounding is allowed.) Types like gaussian random variables that do not have a strict lower bound should use some characteristic lower and upper values, e.g., `center ± σ`.
- [`loval(x)`](@ref): should return the lower span.
- [`hival(x)`](@ref): should return the upper span.
- arithmetic operations, e.g., `+`, `-`, `*`, and `/`
Note that some ThickNumber subtypes might need additional arguments, so there may be some that cannot be constructed generically and for which `lohi` and `midrad` might error.

The implementation of these functions must satisfy certain requirements spelled out in the documentation of each of these functions.
### Comparison operators

There are also numerous optional methods you can specialize if it makes `MyType` operate more
efficiently. For example, a gaussian random variable package might want to implement [`midrad(MyType{T}, center, σ)`](@ref) to construct values directly, assuming this is the natural parametrization
of this type.
For an explanation of why these aren't just `==`, `<`, etc, read [The Fundamental Principle of ThickNumbers (FPTN)](@ref).

## Ensuring compliance with the ThickNumbers interface
- [`iseq_tn`](@ref) (i.e., `iseq(X, Y)`, or the unicode analog `X ⩦ Y`) checks equality between `X` and `Y` (i.e., the replacement for `x == y`)
- `isequal_tn(X, Y)` replaces `isequal(x, y)` (see the Julia docs for the subtle difference between `==` and `isequal`).
- `isapprox_tn(X, Y)` (unicode `X ⩪ Y`) checks approximate equality (``) between `X` and `Y`
- `X ≺ Y` (typed with `\prec`-TAB) and `X ≻ Y` (`\succ`-TAB) test whether "all" values in `X` are strictly less or greater, respectively, than "all" values in `Y`.
- `isless_tn(X, Y)` replaces `isless(x, y)` (see the Julia docs for the subtle difference between `<` and `isless`)
- `X ⪯ Y` (`\preceq`-TAB) and `X ⪰ Y`(`\succeq`-TAB) replace `<=` and `>=`, respectively.

The `ThickNumbersInterfaceTests` package can be used to determine whether your implementations comply with the requirements. As it is possible that this test suite will evolve and add new requirements,
be sure to use `[compat]` bounds to specify the major version number of `ThickNumbersInterfaceTests` that your implementation satisfies.
### Set operations

## Features provided by subtyping ThickNumber
- [`issubset_tn`](@ref) replaces `` (unicode: ``)
- [`is_strict_subset_tn`](@ref) replaces `` (unicode: ``)
- [`issupset_tn`](@ref) replaces `` (unicode: ``)
- [`is_strict_supset_tn`](@ref) replaces `` (unicode: ``)
- [`hull`](@ref) creates a number that contains its arguments

See the [User API](@ref).
### API reference

## API
The API is described more completely in:

```@contents
Pages = ["required.md", "optional.md", "user_api.md"]
Expand Down
19 changes: 13 additions & 6 deletions docs/src/required.md
Original file line number Diff line number Diff line change
@@ -1,13 +1,20 @@
# Required API functions

Each package creating a new `ThickNumber` subtype must implement these functions.

You also need to implement any binary arithmetic operations (`a + b`, `a - b`, `a * b`, `a / b`).
Unary `+` and `-` (i.e., `-x`) have default implementations for all `ThickNumber` subtypes,
although you can choose to specialize if warranted.
Each package creating a new `ThickNumber` subtype must implement:

```@docs
lohi
loval
hival
```

If possible, you should also implement:

```@docs
lohi
```

If your ThickNumber subtype can't be constructed this way, you will likely have to specialize several of the ThickNumber API functions to compensate.

You also need to implement any binary arithmetic operations (`a + b`, `a - b`, `a * b`, `a / b`).
Unary `+` and `-` (i.e., `-x`) have default implementations for all `ThickNumber` subtypes,
although you can choose to specialize if warranted.
13 changes: 13 additions & 0 deletions docs/src/user_api.md
Original file line number Diff line number Diff line change
Expand Up @@ -33,6 +33,19 @@ isinf_tn
isnan_tn
```

## Comparison operators

```@docs
iseq_tn
isequal_tn
isapprox_tn
isless_tn
```

## Set operations

See also [IntervalSets](https://github.com/JuliaMath/IntervalSets.jl) for a more flexible way of supporting intervals as sets.
Expand Down
68 changes: 62 additions & 6 deletions src/ThickNumbers.jl
Original file line number Diff line number Diff line change
Expand Up @@ -17,7 +17,7 @@ export lohi, midrad, loval, hival, mid, wid, rad, mag, mig
export emptyset, hull, issubset_tn, , is_strict_subset_tn, , issupset_tn, , is_strict_supset_tn,

# Operators
export isequal_tn, , isapprox_tn, , isless_tn, ,
export isequal_tn, iseq_tn, , isapprox_tn, , isless_tn, , , ,

# Unary
export isfinite_tn, isinf_tn, isnan_tn
Expand All @@ -29,7 +29,14 @@ abstract type ThickNumber{T<:Number} <: Number end
"""
valuetype(::Type{<:ThickNumber})
Return the `T` in `ThickNumber{T}`.
Return the type of the numbers in the span, i.e., the `T` in `ThickNumber{T}`.
# Examples
```julia
julia> valuetype(Interval{Float64})
Float64
```
"""
valuetype(::Type{TN}) where TN<:ThickNumber{T} where T = T
valuetype(x::ThickNumber) = valuetype(typeof(x))
Expand Down Expand Up @@ -354,11 +361,28 @@ hull(a::TN, b::TN) where TN<:ThickNumber =

## Operators

# The valuetype should not have to be the same, but there doesn't
# seem to be a way of expressing that.
isequal_tn(a::ThickNumber, b::ThickNumber) = (isempty(a) & isempty(b)) | ((loval(a) == loval(b)) & (hival(a) == hival(b)))
const = isequal_tn
"""
isequal_tn(a::ThickNumber, b::ThickNumber)
Returns `true` if `a` and `b` are both empty or both `loval` and `hival` are equal in the sense of `isequal`. It is `false` otherwise.
"""
isequal_tn(a::ThickNumber, b::ThickNumber) = (isempty(a) & isempty(b)) | (isequal(loval(a), loval(b)) & isequal(hival(a), hival(b)))

"""
iseq_tn(a::ThickNumber, b::ThickNumber)
a ⩦ b
Returns `true` if `a` and `b` are both empty or both `loval` and `hival` are equal in the sense of `==`. It is `false` otherwise.
"""
iseq_tn(a::ThickNumber, b::ThickNumber) = (isempty(a) & isempty(b)) | ((loval(a) == loval(b)) & (hival(a) == hival(b)))
const = iseq_tn

"""
isapprox_tn(a::ThickNumber, b::ThickNumber; atol=0, rtol::Real=atol>0 ? 0 : √eps)
a ⩪ b
Returns `true` if `a` and `b` are both empty or both `loval` and `hival` are approximately equal (≈). It is `false` otherwise.
"""
function isapprox_tn(x::ThickNumber, y::ThickNumber; atol::Real=0, rtol::Real=Base.rtoldefault(x,y,atol), nans::Bool=false)
isempty(x) && isempty(y) && return true
isequal_tn(x, y) || (isfinite_tn(x) && isfinite_tn(y) && max(abs(hival(x)-hival(y)), abs(loval(x)-loval(y))) <= max(atol, rtol*max(mag(x), mag(y)))) || (nans && isnan(x) && isnan(y))
Expand All @@ -370,10 +394,42 @@ end
Base.rtoldefault(::Type{TN}) where TN<:ThickNumber{T} where T = Base.rtoldefault(T)
const = isapprox_tn

"""
isless_tn(a::ThickNumber, b::ThickNumber)
Returns `true` if `isless(hival(a), loval(b))`, `false` otherwise. See also [`≺`](@ref).
"""
isless_tn(a::ThickNumber, b::ThickNumber) = isless(hival(a), loval(b))

"""
a ≺ b
Returns `true` if `hival(a) < loval(b)`, `false` otherwise. Use `\\prec`-TAB to type.
"""
(a::ThickNumber, b::ThickNumber) = hival(a) < loval(b)

"""
a ≻ b
Returns `true` if `loval(a) > hival(b)`, `false` otherwise. Use `\\succ`-TAB to type.
"""
(a::ThickNumber, b::ThickNumber) = hival(a) > loval(b)

"""
a ≼ b
Returns `true` if `hival(a) ≤ loval(b)`, `false` otherwise. Use `\\preceq`-TAB to type.
"""
(a::ThickNumber, b::ThickNumber) = hival(a) < loval(b)

"""
a ≽ b
Returns `true` if `loval(a) ≥ hival(b)`, `false` otherwise. Use `\\succeq`-TAB to type.
"""
(a::ThickNumber, b::ThickNumber) = hival(a) > loval(b)


## Unary + and -
Base.:(+)(a::ThickNumber) = a
Base.:(-)(a::TN) where TN<:ThickNumber = lohi(TN, -hival(a), -loval(a))
Expand Down

0 comments on commit 64f17fb

Please sign in to comment.