Packages

Pure Elixir implementation of Walker's Alias Method (WAM). It's method for performing weighted random sampling.

Current section

Files

Jump to
wam README.md
Raw

README.md

# WAM
Pure Elixir implementation of Walker's Alias Method (WAM). It's method for performing weighted random sampling.
## Installation
If [available in Hex](https://hex.pm/docs/publish), the package can be installed
by adding `wam` to your list of dependencies in `mix.exs`:
```elixir
def deps do
[
{:wam, "~> 0.3.0"}
]
end
```
## Upgrading from 0.2.x
**`new/1` and `new/2` now raise `ArgumentError` when the weights sum to `0`.** It used to answer a table instead, and that table drew *uniformly* over values every one of which had been muted. The bump is minor rather than patch because the change is breaking: where a caller got a structure back, it now gets an exception.
There is no honest table for that input. Muting by weight `0` works because the muted slot is paired away with a slot heavier than the mean; with no positive weight there is no heavy slot, nothing rewrites an alias, and every slot keeps the identity alias it started with. A slot aliased to itself is returned by the draw whatever `probs` holds, so unreachability — the very thing the weights asked for — is not a state the structure can express. Answering `probs = 0.0` does not fix it; only refusing does.
Integer `0` and float `0.0` are the same case now. The old clause stood on a literal `0` in the head, which does not match `0.0`, so float zeros fell through to the general clause and died with an `ArithmeticError` — one meaning, two incompatible behaviours.
A weight of `0` beside at least one positive weight is untouched and still mutes its slot by construction.
**`index/3` now checks the index in both directions.** It used to guard only `index < size`, so a negative index reached `elem/2` and raised `ArgumentError` instead of the `{:error, :invalid_index}` its spec promised — and through `fetch/3` and `get/4` the same. All four now answer the error tuple, and `get/4` its default, for anything that is not an integer in `[0, size)`. `fetch_at/2` and `at/3` already behaved this way.
## Upgrading from 0.1.x
**The meaning of the public `probs` field has flipped.** It used to hold the
probability of taking the *alias*; it now holds the probability of **keeping**
the bucket, so a draw reads `if rng < probs[i], do: i, else: aliases[i]`. Sampling
through `fetch/3`, `get/4` and `index/3` behaves exactly as before — only code
that reads `wam.probs` directly, or that passes a hand-picked `rng` to steer the
comparison, is affected.
The reason for the change is a guarantee: a zero-weight atom is always paired
away, so its `probs` entry is exactly `0.0`, and `rng < 0.0` is false for **any**
`rng >= 0`. A weight of `0` is therefore unreachable by construction, no longer
depending on the generator staying strictly below `1.0`. Code that mutes content
by weighting it `0` can rely on that.
If you were calling `get(wam, index, sentinel_rng)` to read a slot without
drawing, use `at/3` instead — see below.
## Usage
It supports both weights and probabilities.
```elixir
wam = WAM.new(%{a: 10, b: 4, c: 5})
wam = WAM.new(%{a: 10 / 19, b: 4 / 19, c: 5 / 19})
```
Also it supports list of tuples and two lists:
```elixir
wam = WAM.new([{:a, 10}, {:b, 4}, {:c, 5}])
wam = WAM.new([:a, :b, :c], [10, 4, 5])
```
And provides three functions to work with random sampling:
- `fetch(wam, index, random)` - to get weighted value in form of `{:ok, value}` or return `{:error, reason}` tuple in case of error.
```elixir
{:ok, value} = WAM.fetch(wam, :rand.uniform(wam.size) - 1, :rand.uniform())
{:error, reason} = WAM.fetch(wam, :rand.uniform(wam.size) + 1, :rand.uniform())
```
- `fetch(wam, index, random, default \\ nil)` - to get weighted value or default value in case of error. The default value is `nil` unless otherwise specified.
```elixir
value = WAM.get(wam, :rand.uniform(wam.size) - 1, :rand.uniform())
nil = WAM.get(wam, :rand.uniform(wam.size) + 1, :rand.uniform())
value = WAM.get(wam, :rand.uniform(wam.size) - 1, :rand.uniform(), :default)
:default = WAM.get(wam, :rand.uniform(wam.size) + 1, :rand.uniform(), :default)
```
- `index(wam, index, random)` - to get weighted index in form of `{:ok, value}` or return `{:error, reason}` tuple in case of error
```elixir
{:ok, index} = WAM.fetch(wam, :rand.uniform(wam.size) - 1, :rand.uniform())
{:error, reason} = WAM.fetch(wam, :rand.uniform(wam.size) + 1, :rand.uniform())
```
And two functions to read a slot without drawing at all:
- `at(wam, index, default \\ nil)` and `fetch_at(wam, index)`
```elixir
:a = WAM.at(wam, 0)
{:ok, :a} = WAM.fetch_at(wam, 0)
{:error, :invalid_index} = WAM.fetch_at(wam, wam.size)
```
Reach for these rather than passing an `rng` picked to defeat the alias — that
trick depends on how `probs` is oriented and breaks silently when it changes.
Documentation can be generated with [ExDoc](https://github.com/elixir-lang/ex_doc)
and published on [HexDocs](https://hexdocs.pm). Once published, the docs can
be found at <https://hexdocs.pm/wam>.