AstroUtils.CircularStats (astro_utils v0.1.0)

Copy Markdown View Source

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

Types

A covering arc: {:arc, extent_degrees, start_degrees}.

An angle in degrees.

Functions

Mean direction of a list of angles, returned in [0, 360).

Shortest arc that contains every sample.

Types

arc()

@type arc() :: {:arc, degree(), degree()}

A covering arc: {:arc, extent_degrees, start_degrees}.

Sweeping extent_degrees from start_degrees in the direction of increasing longitude covers every input sample.

degree()

@type degree() :: float()

An angle in degrees.

Functions

circular_mean(degrees)

@spec circular_mean([degree()]) :: degree() | :undefined

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

minimal_covering_arc(degrees)

@spec minimal_covering_arc([degree()]) :: arc()

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}