Circular mean and minimal covering arc for sets of angles on a circle.
Ordinary averaging breaks down on a circle: the arithmetic mean of 350° and 10° is 180°, the exact opposite of the answer you want. These functions work on the circle, so the same pair averages to 0°.
Inputs are degrees and need not be normalized; each function normalizes to
[0, 360) internally. Both functions require a non-empty list and raise
FunctionClauseError otherwise. circular_mean/1 returns :undefined for
sample sets that have no mean direction.
iex> AstroUtils.CircularStats.circular_mean([350.0, 10.0])
0.0
Summary
Functions
Mean direction of a list of angles, returned in [0, 360).
Shortest arc that contains every sample.
Types
Functions
Mean direction of a list of angles, returned in [0, 360).
The mean is always the direction of the resultant vector Σ(cos θ, sin θ),
so samples are weighted by direction rather than by count. A lopsided set
inside a half-circle such as [0.0, 10.0, 170.0] therefore means to about
19.15°, not to the arithmetic 60.0.
Returns :undefined when the resultant vector vanishes and no mean
direction exists — antipodal pairs such as [90.0, 270.0], or evenly
spread samples such as [0.0, 120.0, 240.0]. Callers must handle this
case; it is deliberately not a number.
Raises FunctionClauseError on an empty list.
Examples
iex> AstroUtils.CircularStats.circular_mean([350.0, 10.0])
0.0
iex> AstroUtils.CircularStats.circular_mean([0.0, 10.0, 170.0]) |> Float.round(4)
19.1519
iex> AstroUtils.CircularStats.circular_mean([90.0, 270.0])
:undefined
Shortest arc that contains every sample.
Found by locating the largest empty gap between consecutive samples around
the circle; the covering arc is the complement of that gap. Returns
{:arc, extent_degrees, start_degrees}, where start_degrees is the sample
just after the largest gap. If several gaps tie for largest, the smallest
candidate start is used.
A single sample, or repeats of one value, gives an arc of extent 0.0.
Raises FunctionClauseError on an empty list.
Examples
iex> AstroUtils.CircularStats.minimal_covering_arc([5.0, 15.0, 25.0])
{:arc, 20.0, 5.0}
iex> AstroUtils.CircularStats.minimal_covering_arc([350.0, 10.0, 20.0])
{:arc, 30.0, 350.0}
iex> AstroUtils.CircularStats.minimal_covering_arc([180.0])
{:arc, 0.0, 180.0}