Packages

Log probabilities made simple

Current section

Files

Jump to
logprob lib log_prob.ex
Raw

lib/log_prob.ex

defmodule LogProb do
@moduledoc """
A library for managing log probability values.
LogProb provides a decimal-like API for working with log probabilities,
including conversion to percentages and probabilities.
"""
defstruct [:value]
@type t :: %LogProb{value: Decimal.t()}
@doc """
Creates a new LogProb from a number or string.
## Examples
iex> LogProb.new(-2.3026)
%LogProb{value: Decimal.new("-2.3026")}
iex> LogProb.new("-inf")
%LogProb{value: Decimal.new("-Infinity")}
"""
def new(value) when is_number(value) do
%LogProb{value: Decimal.from_float(value)}
end
def new(value) when is_binary(value) do
%LogProb{value: Decimal.new(value)}
end
def new(%Decimal{} = value) do
%LogProb{value: value}
end
@doc """
Creates a new LogProb from a probability value (0.0 to 1.0).
## Examples
iex> LogProb.from_probability(0.1)
#LogProb<-2.3025850929940455>
iex> LogProb.from_probability(0.0)
#LogProb<-Infinity>
"""
def from_probability(prob) when is_number(prob) do
cond do
prob == 0.0 -> %LogProb{value: Decimal.new("-Infinity")}
prob == 1.0 -> %LogProb{value: Decimal.new("0")}
prob > 0.0 and prob < 1.0 -> %LogProb{value: Decimal.from_float(:math.log(prob))}
true -> raise ArgumentError, "Probability must be between 0.0 and 1.0"
end
end
@doc """
Compares two LogProb values.
## Examples
iex> a = LogProb.new(-1.0)
iex> b = LogProb.new(-2.0)
iex> LogProb.compare(a, b)
:gt
"""
def compare(%LogProb{value: a}, %LogProb{value: b}) do
Decimal.compare(a, b)
end
@doc """
Converts LogProb to probability (0.0 to 1.0 range).
## Examples
iex> logprob = LogProb.new(-0.6931471805599453)
iex> LogProb.to_probability(logprob)
0.5
iex> logprob = LogProb.new("-Infinity")
iex> LogProb.to_probability(logprob)
0.0
"""
def to_probability(%LogProb{value: val}) do
if Decimal.equal?(val, Decimal.new("-Infinity")) do
0.0
else
:math.exp(Decimal.to_float(val))
end
end
@doc """
Converts LogProb to percentage with options.
## Options
- `decimals`: Number of decimal places (default: 2)
- `clamp`: Whether to clamp between 0 and 100 (default: true)
- `percentage_symbol`: Whether to include % symbol (default: false)
## Examples
iex> logprob = LogProb.new(-2.3026)
iex> LogProb.to_percent(logprob)
"10.00"
iex> logprob = LogProb.new(-2.3026)
iex> LogProb.to_percent(logprob, percentage_symbol: true)
"10.00%"
iex> logprob = LogProb.new(-2.3026)
iex> LogProb.to_percent(logprob, decimals: 1)
"10.0"
"""
def to_percent(%LogProb{} = logprob, opts \\ []) do
decimals = Keyword.get(opts, :decimals, 2)
clamp = Keyword.get(opts, :clamp, true)
percentage_symbol = Keyword.get(opts, :percentage_symbol, false)
prob = to_probability(logprob)
percent = prob * 100.0
clamped_percent =
if clamp do
max(0.0, min(100.0, percent))
else
percent
end
formatted = :erlang.float_to_binary(clamped_percent, [{:decimals, decimals}])
if percentage_symbol do
formatted <> "%"
else
formatted
end
end
@doc """
Returns the raw Decimal value.
## Examples
iex> logprob = LogProb.new(-2.3026)
iex> LogProb.to_decimal(logprob)
Decimal.new("-2.3026")
"""
def to_decimal(%LogProb{value: val}), do: val
@doc """
Converts LogProb to string.
## Examples
iex> logprob = LogProb.new(-2.3026)
iex> LogProb.to_string(logprob)
"-2.3026"
"""
def to_string(%LogProb{value: val}), do: Decimal.to_string(val)
defimpl String.Chars, for: LogProb do
def to_string(logprob), do: LogProb.to_string(logprob)
end
defimpl Inspect, for: LogProb do
def inspect(%LogProb{value: val}, _opts) do
"#LogProb<#{Decimal.to_string(val)}>"
end
end
end