///|
pub(all) struct CriterionComparison {
criterion : String
left_value : Double
right_value : Double
oriented_difference : Double
weighted_preference_left : Double
weighted_preference_right : Double
} derive(@debug.Debug)
///|
pub(all) struct PairExplanation {
left : String
right : String
criteria : Array[CriterionComparison]
aggregate_left : Double
aggregate_right : Double
net_preference : Double
left_dominates : Bool
right_dominates : Bool
} derive(@debug.Debug)
///|
pub fn explain_pair(
m : DecisionMatrix,
weights : Array[Double],
preferences : Array[Preference],
left : Int32,
right : Int32,
) -> PairExplanation raise ChoiceError {
let w = matrix_weights(m, weights)
preference_specs(m, preferences)
if left < 0 ||
left >= m.names.length() ||
right < 0 ||
right >= m.names.length() {
raise Invalid("alternative index out of bounds")
}
let criteria : Array[CriterionComparison] = []
for j in 0.. c.weighted_preference_left))
let aggregate_right = sum(criteria.map(c => c.weighted_preference_right))
{
left: m.names[left],
right: m.names[right],
criteria,
aggregate_left,
aggregate_right,
net_preference: aggregate_left - aggregate_right,
left_dominates: dominates(m, left, right),
right_dominates: dominates(m, right, left),
}
}
///|
pub(all) struct AdditiveBreakEven {
intercept : Double
slope : Double
crossing : Double?
tied_for_all_weights : Bool
} derive(@debug.Debug)
///|
/// Exact SAW one-coordinate weight-grid crossing for a fixed normalization matrix.
pub fn additive_break_even(
m : DecisionMatrix,
weights : Array[Double],
criterion : Int32,
left : Int32,
right : Int32,
) -> AdditiveBreakEven raise ChoiceError {
let w = matrix_weights(m, weights)
if w.length() < 2 {
raise Invalid("break-even sweep requires at least two criteria")
}
if criterion < 0 || criterion >= w.length() {
raise Invalid("criterion index out of range")
}
let _ = m.cell(left, criterion)
let _ = m.cell(right, criterion)
let x = m.normalize(MinMax)
let rest = sum([ for j in 0.. w[j] ])
let mut intercept = 0.0
for j in 0.. 0.0 {
w[j] / rest
} else {
1.0 / (w.length() - 1).to_double()
}
intercept += share * (x[left][j] - x[right][j])
}
let slope = x[left][criterion] - x[right][criterion] - intercept
let root = if slope == 0.0 {
None
} else {
let value = -intercept / slope
if value >= 0.0 && value <= 1.0 {
Some(value)
} else {
None
}
}
{
intercept,
slope,
crossing: root,
tied_for_all_weights: intercept == 0.0 && slope == 0.0,
}
}
///|
pub extend CriterionComparison with @debug.Debug::{to_repr}
///|
pub extend PairExplanation with @debug.Debug::{to_repr}
///|
pub extend AdditiveBreakEven with @debug.Debug::{to_repr}