///|
pub struct DecisionPolicy {
priv conflict_ceiling : Double
priv nonspecific_ceiling : Double
priv required_margin : Double
} derive(Debug, Eq)
///|
pub extend DecisionPolicy with @debug.Debug::{to_repr}
///|
pub extend DecisionPolicy with Eq::{equal, not_equal}
///|
pub fn decision_policy(
max_conflict~ : Double = 0.5,
max_nonspecific~ : Double = 0.8,
min_margin~ : Double = 0.05,
) -> DecisionPolicy raise BeliefError {
if !finite(max_conflict) ||
max_conflict < 0.0 ||
max_conflict > 1.0 ||
!finite(max_nonspecific) ||
max_nonspecific < 0.0 ||
max_nonspecific > 1.0 ||
!finite(min_margin) ||
min_margin < 0.0 {
raise Invalid(
"decision thresholds must be finite, nonnegative, and masses at most one",
)
}
{
conflict_ceiling: max_conflict,
nonspecific_ceiling: max_nonspecific,
required_margin: min_margin,
}
}
///|
pub struct ActionDecision {
index : Int32
label : String
expected_score : Double
margin : Double
accepted : Bool
reason : String
conflict : Double
nonspecific : Double
} derive(Debug, Eq)
///|
pub extend ActionDecision with @debug.Debug::{to_repr}
///|
pub extend ActionDecision with Eq::{equal, not_equal}
///|
/// Rows are actions; columns are hypotheses in frame order. Limited to 64
/// actions and finite magnitudes <=1e12. Returned expectations use BetP.
pub fn Mass::expected_utilities(
self : Mass,
utilities : Array[Array[Double]],
) -> Array[Double] raise BeliefError {
if utilities.length() == 0 || utilities.length() > 64 {
raise Invalid("utility matrix requires 1..64 actions")
}
let probabilities = self.pignistic()
let result : Array[Double] = []
for row in utilities {
if row.length() != self.domain.size() {
raise Invalid("utility row must match hypothesis count")
}
let terms : Array[Double] = []
for i = 0; i < row.length(); i = i + 1 {
let value = row[i]
if !finite(value) || magnitude(value) > 1.0e12 {
raise Invalid(
"utility entries must be finite and magnitude at most 1e12",
)
}
terms.push(value * probabilities[i])
}
result.push(stable_sum(terms))
}
result
}
///|
/// Loss rows have the same layout, but must be nonnegative.
pub fn Mass::expected_losses(
self : Mass,
losses : Array[Array[Double]],
) -> Array[Double] raise BeliefError {
if losses.length() == 0 || losses.length() > 64 {
raise Invalid("loss matrix requires 1..64 actions")
}
for row in losses {
if row.length() != self.domain.size() {
raise Invalid("loss row must match hypothesis count")
}
for value in row {
if value < 0.0 {
raise Invalid("loss entries must be nonnegative")
}
}
}
self.expected_utilities(losses)
}
///|
/// Explicit supplied policy; equal scores keep the first action.
/// If only one action exists its margin is defined as zero, not infinity.
/// Margins have utility units, so caller must calibrate the policy accordingly.
pub fn Mass::decide(
self : Mass,
actions : Array[String],
utilities : Array[Array[Double]],
policy : DecisionPolicy,
) -> ActionDecision raise BeliefError {
let scores = self.expected_utilities(utilities)
if actions.length() != scores.length() {
raise Invalid("action labels and utility rows must have equal counts")
}
for i = 0; i < actions.length(); i = i + 1 {
if actions[i].length() == 0 || actions[i].length() > 256 {
raise Invalid("action labels must have 1..256 characters")
}
for j = 0; j < i; j = j + 1 {
if actions[i] == actions[j] {
raise Invalid("action labels must be unique")
}
}
}
let mut best = 0
for i = 1; i < scores.length(); i = i + 1 {
if scores[i] > scores[best] {
best = i
}
}
let mut runner_up = scores[best]
let mut has_runner = false
for i = 0; i < scores.length(); i = i + 1 {
if i != best && (!has_runner || scores[i] > runner_up) {
runner_up = scores[i]
has_runner = true
}
}
let margin = scores[best] - runner_up
let conflict = self.conflict()
let nonspecific = self.nonspecific_mass()
let reason = if conflict > policy.conflict_ceiling {
"conflict exceeds policy"
} else if nonspecific > policy.nonspecific_ceiling {
"nonspecificity exceeds policy"
} else if margin < policy.required_margin {
"utility margin below policy"
} else {
"accepted by supplied policy"
}
{
index: best,
label: actions[best],
expected_score: scores[best],
margin,
accepted: conflict <= policy.conflict_ceiling &&
nonspecific <= policy.nonspecific_ceiling &&
margin >= policy.required_margin,
reason,
conflict,
nonspecific,
}
}