use serde::{Deserialize, Serialize};
use crate::error::TopologyError;
use crate::record::RecordSet;
use crate::topology::Topology;
#[derive(Debug, Clone, Copy, PartialEq, Serialize, Deserialize)]
#[serde(tag = "mode", rename_all = "snake_case")]
pub enum ProxyConditioning {
Global,
Local { temperature: f32 },
}
impl Default for ProxyConditioning {
fn default() -> Self {
ProxyConditioning::Local { temperature: 8.0 }
}
}
#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
pub struct ProxyConfig {
pub ridge: f32,
pub conditioning: ProxyConditioning,
}
impl Default for ProxyConfig {
fn default() -> Self {
Self {
ridge: 1.0,
conditioning: ProxyConditioning::default(),
}
}
}
#[derive(Debug, Clone, Copy, PartialEq, Serialize, Deserialize)]
pub struct ProxyScore {
pub utility: f32,
pub cost: f32,
}
impl ProxyScore {
pub fn objective(&self, cost_weight: f32) -> f32 {
self.utility - cost_weight * self.cost
}
}
#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
enum Fitted {
Global {
utility_weights: Vec<f32>,
cost_weights: Vec<f32>,
},
Local {
design: Vec<Vec<f32>>,
directions: Vec<Vec<f32>>,
has_direction: Vec<bool>,
utility_targets: Vec<f32>,
cost_targets: Vec<f32>,
ridge: f32,
temperature: f32,
},
}
#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
#[serde(try_from = "ExecutionProxyWire")]
pub struct ExecutionProxy {
fitted: Fitted,
n: usize,
query_dim: usize,
}
#[derive(Deserialize)]
struct ExecutionProxyWire {
fitted: Fitted,
n: usize,
query_dim: usize,
}
impl TryFrom<ExecutionProxyWire> for ExecutionProxy {
type Error = TopologyError;
fn try_from(wire: ExecutionProxyWire) -> Result<Self, Self::Error> {
let proxy = ExecutionProxy {
fitted: wire.fitted,
n: wire.n,
query_dim: wire.query_dim,
};
proxy.validate()?;
Ok(proxy)
}
}
#[derive(Debug, Clone, PartialEq)]
pub struct ConditionedProxy {
utility_weights: Vec<f32>,
cost_weights: Vec<f32>,
includes_query: bool,
n: usize,
}
impl ConditionedProxy {
pub fn score(&self, topology: &Topology, query: &[f32]) -> Result<ProxyScore, TopologyError> {
if topology.n() != self.n {
return Err(TopologyError::SizeMismatch {
expected: self.n,
found: topology.n(),
});
}
let x = features(topology, query, self.includes_query);
Ok(ProxyScore {
utility: dot(&x, &self.utility_weights),
cost: dot(&x, &self.cost_weights),
})
}
}
impl ExecutionProxy {
pub fn fit(records: &RecordSet, config: &ProxyConfig) -> Result<Self, TopologyError> {
if !config.ridge.is_finite() || config.ridge < 0.0 {
return Err(TopologyError::BadConfig {
field: "ridge",
expected: "finite and non-negative",
found: format!("{}", config.ridge),
});
}
let n = records.team_size();
let query_dim = records.query_dim();
let include_query = matches!(config.conditioning, ProxyConditioning::Global);
let mut design = Vec::with_capacity(records.len());
let mut utility_targets = Vec::with_capacity(records.len());
let mut cost_targets = Vec::with_capacity(records.len());
for (index, record) in records.records().iter().enumerate() {
design.push(features(&record.topology, &record.query, include_query));
utility_targets.push(record.utility);
cost_targets.push(records.normalized_cost(index));
}
let fitted = match config.conditioning {
ProxyConditioning::Global => {
let dim = feature_dim(n, query_dim, true);
let weights = vec![1.0f64; design.len()];
let gram = gram_matrix(&design, dim, &weights, config.ridge as f64);
let utility_weights = solve(&gram, &rhs(&design, &utility_targets, &weights, dim))?;
let cost_weights = solve(&gram, &rhs(&design, &cost_targets, &weights, dim))?;
Fitted::Global {
utility_weights: utility_weights.into_iter().map(|w| w as f32).collect(),
cost_weights: cost_weights.into_iter().map(|w| w as f32).collect(),
}
}
ProxyConditioning::Local { temperature } => {
if !temperature.is_finite() {
return Err(TopologyError::BadConfig {
field: "temperature",
expected: "finite",
found: format!("{temperature}"),
});
}
let mut directions = Vec::with_capacity(records.len());
let mut has_direction = Vec::with_capacity(records.len());
for record in records.records() {
let (d, has) = normalize(&record.query);
directions.push(d);
has_direction.push(has);
}
Fitted::Local {
design,
directions,
has_direction,
utility_targets,
cost_targets,
ridge: config.ridge,
temperature,
}
}
};
Ok(Self {
fitted,
n,
query_dim,
})
}
pub fn validate(&self) -> Result<(), TopologyError> {
if self.n < 2 {
return Err(TopologyError::TeamTooSmall { n: self.n });
}
let expect = |field: &'static str, found: usize, want: usize| {
if found == want {
Ok(())
} else {
Err(TopologyError::BadConfig {
field,
expected: "one weight per feature",
found: format!("{found} for {want} features"),
})
}
};
match &self.fitted {
Fitted::Global {
utility_weights,
cost_weights,
} => {
let want = feature_dim(self.n, self.query_dim, true);
expect("utility_weights", utility_weights.len(), want)?;
expect("cost_weights", cost_weights.len(), want)?;
}
Fitted::Local {
design,
directions,
has_direction,
utility_targets,
cost_targets,
ridge,
temperature,
} => {
if design.is_empty() {
return Err(TopologyError::NoRecords { kind: "proxy" });
}
let rows = design.len();
for (field, found) in [
("directions", directions.len()),
("has_direction", has_direction.len()),
("utility_targets", utility_targets.len()),
("cost_targets", cost_targets.len()),
] {
if found != rows {
return Err(TopologyError::BadConfig {
field,
expected: "one entry per design row",
found: format!("{found} for {rows} rows"),
});
}
}
let want = feature_dim(self.n, self.query_dim, false);
for row in design {
expect("design row", row.len(), want)?;
}
for direction in directions {
if direction.len() != self.query_dim {
return Err(TopologyError::QueryDimMismatch {
expected: self.query_dim,
found: direction.len(),
});
}
}
if !ridge.is_finite() || *ridge < 0.0 {
return Err(TopologyError::BadConfig {
field: "ridge",
expected: "finite and non-negative",
found: format!("{ridge}"),
});
}
if !temperature.is_finite() {
return Err(TopologyError::BadConfig {
field: "temperature",
expected: "finite",
found: format!("{temperature}"),
});
}
}
}
Ok(())
}
pub fn team_size(&self) -> usize {
self.n
}
pub fn query_dim(&self) -> usize {
self.query_dim
}
pub fn condition(&self, query: &[f32]) -> Result<ConditionedProxy, TopologyError> {
if query.len() != self.query_dim {
return Err(TopologyError::QueryDimMismatch {
expected: self.query_dim,
found: query.len(),
});
}
match &self.fitted {
Fitted::Global {
utility_weights,
cost_weights,
} => Ok(ConditionedProxy {
utility_weights: utility_weights.clone(),
cost_weights: cost_weights.clone(),
includes_query: true,
n: self.n,
}),
Fitted::Local {
design,
directions,
has_direction,
utility_targets,
cost_targets,
ridge,
temperature,
} => {
let (direction, query_has_direction) = normalize(query);
let logits: Vec<f32> = directions
.iter()
.zip(has_direction.iter())
.map(|(d, &has)| {
if has && query_has_direction {
temperature * dot(&direction, d)
} else {
0.0
}
})
.collect();
let softmaxed = softmax(&logits);
let weights: Vec<f64> = softmaxed
.iter()
.map(|w| *w as f64 * design.len() as f64)
.collect();
let dim = feature_dim(self.n, self.query_dim, false);
let gram = gram_matrix(design, dim, &weights, *ridge as f64);
let utility_weights = solve(&gram, &rhs(design, utility_targets, &weights, dim))?;
let cost_weights = solve(&gram, &rhs(design, cost_targets, &weights, dim))?;
Ok(ConditionedProxy {
utility_weights: utility_weights.into_iter().map(|w| w as f32).collect(),
cost_weights: cost_weights.into_iter().map(|w| w as f32).collect(),
includes_query: false,
n: self.n,
})
}
}
}
pub fn score(&self, topology: &Topology, query: &[f32]) -> Result<ProxyScore, TopologyError> {
self.condition(query)?.score(topology, query)
}
pub fn score_batch(
&self,
candidates: &[Topology],
query: &[f32],
) -> Result<Vec<ProxyScore>, TopologyError> {
let conditioned = self.condition(query)?;
candidates
.iter()
.map(|t| conditioned.score(t, query))
.collect()
}
}
fn feature_dim(n: usize, query_dim: usize, use_query: bool) -> usize {
1 + n * (n - 1) + if use_query { query_dim } else { 0 }
}
fn features(topology: &Topology, query: &[f32], use_query: bool) -> Vec<f32> {
let mut x = Vec::with_capacity(1 + topology.flat().len() + query.len());
x.push(1.0);
x.extend(topology.flat().iter().map(|&e| if e { 1.0 } else { 0.0 }));
if use_query {
x.extend_from_slice(query);
}
x
}
fn dot(x: &[f32], w: &[f32]) -> f32 {
x.iter().zip(w.iter()).map(|(a, b)| a * b).sum()
}
fn normalize(v: &[f32]) -> (Vec<f32>, bool) {
let norm = v.iter().map(|x| x * x).sum::<f32>().sqrt();
if norm > f32::EPSILON {
(v.iter().map(|x| x / norm).collect(), true)
} else {
(vec![0.0; v.len()], false)
}
}
fn softmax(logits: &[f32]) -> Vec<f32> {
let max = logits.iter().copied().fold(f32::NEG_INFINITY, f32::max);
let exps: Vec<f32> = logits.iter().map(|l| (l - max).exp()).collect();
let total: f32 = exps.iter().sum();
if total > 0.0 {
exps.into_iter().map(|e| e / total).collect()
} else {
vec![1.0 / logits.len() as f32; logits.len()]
}
}
fn gram_matrix(design: &[Vec<f32>], dim: usize, weights: &[f64], ridge: f64) -> Vec<f64> {
let mut gram = vec![0f64; dim * dim];
for (row, &w) in design.iter().zip(weights.iter()) {
if w == 0.0 {
continue;
}
for i in 0..dim {
let xi = row[i] as f64 * w;
if xi == 0.0 {
continue;
}
for j in 0..dim {
gram[i * dim + j] += xi * row[j] as f64;
}
}
}
for i in 0..dim {
if i > 0 {
gram[i * dim + i] += ridge;
}
}
gram
}
fn rhs(design: &[Vec<f32>], targets: &[f32], weights: &[f64], dim: usize) -> Vec<f64> {
let mut b = vec![0f64; dim];
for ((row, &y), &w) in design.iter().zip(targets.iter()).zip(weights.iter()) {
if w == 0.0 {
continue;
}
let wy = y as f64 * w;
for i in 0..dim {
b[i] += row[i] as f64 * wy;
}
}
b
}
fn solve(gram: &[f64], b: &[f64]) -> Result<Vec<f64>, TopologyError> {
let dim = b.len();
let mut jitter = 0f64;
for attempt in 0..8 {
let mut a = gram.to_vec();
if jitter > 0.0 {
for i in 0..dim {
a[i * dim + i] += jitter;
}
}
if let Some(w) = cholesky_solve(&a, b, dim) {
return Ok(w);
}
jitter = if attempt == 0 { 1e-8 } else { jitter * 100.0 };
}
Err(TopologyError::BadConfig {
field: "ridge",
expected: "large enough to make the normal equations solvable",
found: "singular even with jitter".into(),
})
}
fn cholesky_solve(a: &[f64], b: &[f64], dim: usize) -> Option<Vec<f64>> {
let mut l = vec![0f64; dim * dim];
for i in 0..dim {
for j in 0..=i {
let mut sum = a[i * dim + j];
for k in 0..j {
sum -= l[i * dim + k] * l[j * dim + k];
}
if i == j {
if sum <= 0.0 || !sum.is_finite() {
return None;
}
l[i * dim + j] = sum.sqrt();
} else {
l[i * dim + j] = sum / l[j * dim + j];
}
}
}
let mut y = vec![0f64; dim];
for i in 0..dim {
let mut sum = b[i];
for k in 0..i {
sum -= l[i * dim + k] * y[k];
}
y[i] = sum / l[i * dim + i];
}
let mut w = vec![0f64; dim];
for i in (0..dim).rev() {
let mut sum = y[i];
for k in (i + 1)..dim {
sum -= l[k * dim + i] * w[k];
}
w[i] = sum / l[i * dim + i];
}
if w.iter().all(|v| v.is_finite()) {
Some(w)
} else {
None
}
}
#[cfg(test)]
mod tests {
use super::*;
use crate::record::{ExecutionRecord, DEFAULT_COST_WEIGHT};
fn inverted_cost_records() -> RecordSet {
let n = 4;
let chain = Topology::chain(n).unwrap();
let complete = Topology::complete(n).unwrap();
let star = Topology::star(n, 0).unwrap();
let mut out = Vec::new();
for i in 0..8 {
let q = vec![i as f32 / 8.0, 1.0 - i as f32 / 8.0];
let task = format!("t{i}");
out.push(ExecutionRecord::new(
&task,
q.clone(),
chain.clone(),
1.0,
3000,
));
out.push(ExecutionRecord::new(
&task,
q.clone(),
star.clone(),
1.0,
1500,
));
out.push(ExecutionRecord::new(&task, q, complete.clone(), 1.0, 900));
}
RecordSet::new(out).unwrap()
}
fn query_dependent_records() -> RecordSet {
let n = 4;
let chain = Topology::chain(n).unwrap();
let complete = Topology::complete(n).unwrap();
let mut out = Vec::new();
for i in 0..6 {
let drift = i as f32 * 0.01;
let math = format!("math{i}");
let math_q = vec![1.0, 0.0, drift];
out.push(ExecutionRecord::new(
&math,
math_q.clone(),
complete.clone(),
1.0,
600,
));
out.push(ExecutionRecord::new(
&math,
math_q,
chain.clone(),
1.0,
2400,
));
let code = format!("code{i}");
let code_q = vec![0.0, 1.0, drift];
out.push(ExecutionRecord::new(
&code,
code_q.clone(),
chain.clone(),
1.0,
600,
));
out.push(ExecutionRecord::new(
&code,
code_q,
complete.clone(),
1.0,
2400,
));
}
RecordSet::new(out).unwrap()
}
fn global_config(ridge: f32) -> ProxyConfig {
ProxyConfig {
ridge,
conditioning: ProxyConditioning::Global,
}
}
#[test]
fn the_proxy_learns_the_measured_cost_ordering_not_the_edge_count_one() {
let records = inverted_cost_records();
let proxy = ExecutionProxy::fit(&records, &ProxyConfig::default()).unwrap();
let q = vec![0.5, 0.5];
let n = 4;
let chain = proxy.score(&Topology::chain(n).unwrap(), &q).unwrap();
let complete = proxy.score(&Topology::complete(n).unwrap(), &q).unwrap();
assert!(
Topology::chain(n).unwrap().edge_count() < Topology::complete(n).unwrap().edge_count()
);
assert!(
chain.cost > complete.cost,
"chain {} should score dearer than complete {}",
chain.cost,
complete.cost
);
assert!(complete.objective(DEFAULT_COST_WEIGHT) > chain.objective(DEFAULT_COST_WEIGHT));
}
#[test]
fn the_proxy_separates_candidates_on_a_homogeneous_team() {
let records = inverted_cost_records();
for config in [ProxyConfig::default(), global_config(1.0)] {
let proxy = ExecutionProxy::fit(&records, &config).unwrap();
let scores = proxy
.score_batch(
&[
Topology::chain(4).unwrap(),
Topology::star(4, 0).unwrap(),
Topology::complete(4).unwrap(),
],
&[0.5, 0.5],
)
.unwrap();
assert!(scores[0].cost != scores[1].cost, "{config:?}");
assert!(scores[1].cost != scores[2].cost, "{config:?}");
}
}
#[test]
fn local_conditioning_ranks_the_same_topology_differently_per_query() {
let records = query_dependent_records();
let proxy = ExecutionProxy::fit(&records, &ProxyConfig::default()).unwrap();
let n = 4;
let chain = Topology::chain(n).unwrap();
let complete = Topology::complete(n).unwrap();
let math = proxy
.score_batch(&[chain.clone(), complete.clone()], &[1.0, 0.0, 0.0])
.unwrap();
assert!(
math[1].cost < math[0].cost,
"math query should price complete below chain: {math:?}"
);
let code = proxy
.score_batch(&[chain, complete], &[0.0, 1.0, 0.0])
.unwrap();
assert!(
code[0].cost < code[1].cost,
"code query should price chain below complete: {code:?}"
);
}
#[test]
fn global_conditioning_cannot_and_reports_the_average() {
let records = query_dependent_records();
let proxy = ExecutionProxy::fit(&records, &global_config(0.01)).unwrap();
let n = 4;
let candidates = [Topology::chain(n).unwrap(), Topology::complete(n).unwrap()];
let math = proxy.score_batch(&candidates, &[1.0, 0.0, 0.0]).unwrap();
let code = proxy.score_batch(&candidates, &[0.0, 1.0, 0.0]).unwrap();
assert_eq!(
math[0].cost < math[1].cost,
code[0].cost < code[1].cost,
"a global linear fit has no query/topology coupling term"
);
}
#[test]
fn predictions_track_the_targets_they_were_fitted_on() {
let records = inverted_cost_records();
for config in [
ProxyConfig {
ridge: 0.01,
conditioning: ProxyConditioning::Local { temperature: 8.0 },
},
global_config(0.01),
] {
let proxy = ExecutionProxy::fit(&records, &config).unwrap();
for (index, record) in records.records().iter().enumerate() {
let score = proxy.score(&record.topology, &record.query).unwrap();
let target = records.normalized_cost(index);
assert!(
(score.cost - target).abs() < 0.15,
"{config:?} predicted {} for target {target}",
score.cost
);
}
}
}
#[test]
fn utility_head_learns_a_failing_topology() {
let n = 4;
let mut out = Vec::new();
for i in 0..8 {
let q = vec![0.5, 0.5];
let task = format!("t{i}");
out.push(ExecutionRecord::new(
&task,
q.clone(),
Topology::chain(n).unwrap(),
0.0,
1000,
));
out.push(ExecutionRecord::new(
&task,
q,
Topology::complete(n).unwrap(),
1.0,
1000,
));
}
let records = RecordSet::new(out).unwrap();
let proxy = ExecutionProxy::fit(
&records,
&ProxyConfig {
ridge: 0.01,
conditioning: ProxyConditioning::Local { temperature: 8.0 },
},
)
.unwrap();
let q = vec![0.5, 0.5];
let chain = proxy.score(&Topology::chain(n).unwrap(), &q).unwrap();
let complete = proxy.score(&Topology::complete(n).unwrap(), &q).unwrap();
assert!(complete.utility > chain.utility + 0.5);
}
#[test]
fn conditioning_once_matches_scoring_each_candidate() {
let records = query_dependent_records();
let proxy = ExecutionProxy::fit(&records, &ProxyConfig::default()).unwrap();
let q = [0.3, 0.7, 0.02];
let candidates = [
Topology::chain(4).unwrap(),
Topology::star(4, 0).unwrap(),
Topology::complete(4).unwrap(),
];
let batched = proxy.score_batch(&candidates, &q).unwrap();
let conditioned = proxy.condition(&q).unwrap();
for (candidate, expected) in candidates.iter().zip(batched.iter()) {
assert_eq!(&conditioned.score(candidate, &q).unwrap(), expected);
assert_eq!(&proxy.score(candidate, &q).unwrap(), expected);
}
}
#[test]
fn a_degenerate_single_record_set_still_fits() {
let records = RecordSet::new(vec![ExecutionRecord::new(
"t",
vec![1.0, 0.0],
Topology::chain(4).unwrap(),
1.0,
100,
)])
.unwrap();
for config in [ProxyConfig::default(), global_config(1.0)] {
let proxy = ExecutionProxy::fit(&records, &config).unwrap();
let score = proxy
.score(&Topology::complete(4).unwrap(), &[1.0, 0.0])
.unwrap();
assert!(
score.utility.is_finite() && score.cost.is_finite(),
"{config:?}"
);
}
}
#[test]
fn a_zero_query_scores_finitely_under_local_conditioning() {
let records = query_dependent_records();
let proxy = ExecutionProxy::fit(&records, &ProxyConfig::default()).unwrap();
let score = proxy
.score(&Topology::chain(4).unwrap(), &[0.0, 0.0, 0.0])
.unwrap();
assert!(score.utility.is_finite() && score.cost.is_finite());
}
#[test]
fn the_global_fit_carries_query_features_and_the_local_one_does_not() {
let records = inverted_cost_records();
let global = ExecutionProxy::fit(&records, &global_config(1.0)).unwrap();
match &global.fitted {
Fitted::Global {
utility_weights, ..
} => {
assert_eq!(utility_weights.len(), 1 + 4 * 3 + 2);
}
other => panic!("expected a global fit, got {other:?}"),
}
let local = ExecutionProxy::fit(&records, &ProxyConfig::default()).unwrap();
let conditioned = local.condition(&[0.5, 0.5]).unwrap();
assert_eq!(conditioned.utility_weights.len(), 1 + 4 * 3);
assert!(!conditioned.includes_query);
}
#[test]
fn shape_mismatches_are_rejected() {
let records = inverted_cost_records();
let proxy = ExecutionProxy::fit(&records, &ProxyConfig::default()).unwrap();
assert!(matches!(
proxy.score(&Topology::chain(5).unwrap(), &[0.5, 0.5]),
Err(TopologyError::SizeMismatch { .. })
));
assert!(matches!(
proxy.score(&Topology::chain(4).unwrap(), &[0.5]),
Err(TopologyError::QueryDimMismatch { .. })
));
}
#[test]
fn a_negative_ridge_is_rejected() {
let records = inverted_cost_records();
assert!(matches!(
ExecutionProxy::fit(&records, &global_config(-1.0)),
Err(TopologyError::BadConfig { field: "ridge", .. })
));
}
#[test]
fn a_non_finite_temperature_is_rejected() {
let records = inverted_cost_records();
assert!(matches!(
ExecutionProxy::fit(
&records,
&ProxyConfig {
ridge: 1.0,
conditioning: ProxyConditioning::Local {
temperature: f32::NAN
},
}
),
Err(TopologyError::BadConfig {
field: "temperature",
..
})
));
}
#[test]
fn fitting_is_deterministic() {
let records = inverted_cost_records();
for config in [ProxyConfig::default(), global_config(1.0)] {
let a = ExecutionProxy::fit(&records, &config).unwrap();
let b = ExecutionProxy::fit(&records, &config).unwrap();
assert_eq!(a, b);
assert_eq!(
a.score(&Topology::chain(4).unwrap(), &[0.5, 0.5]).unwrap(),
b.score(&Topology::chain(4).unwrap(), &[0.5, 0.5]).unwrap()
);
}
}
}