mod catalog;
mod driver;
#[cfg(test)]
mod tests;
pub(crate) use driver::vtree_from_portfolio;
const DEFAULT_PEAK_TOLERANCE: f64 = 0.10;
#[derive(Clone, Debug, PartialEq)]
pub struct PortfolioKnobs {
pub build_history: PortfolioBuildHistory,
pub seed: u64,
pub trace: TraceLevel,
pub flowcutter_cap_ms: Option<i64>,
pub peak_tolerance: f64,
pub prefer: Option<CandidatePreference>,
pub skip: Vec<&'static str>,
pub ranker: bool,
pub pairwise_weighting: PairwiseWeighting,
}
#[derive(Clone, Copy, Debug, Default, PartialEq, Eq)]
#[non_exhaustive]
pub enum PairwiseWeighting {
#[default]
Candidate,
Family,
}
#[derive(Clone, Debug, PartialEq, Eq)]
#[non_exhaustive]
pub enum CandidatePreference {
Preferred(String),
Required(String),
}
impl CandidatePreference {
pub fn name(&self) -> &str {
match self {
CandidatePreference::Preferred(name) | CandidatePreference::Required(name) => name,
}
}
pub fn is_required(&self) -> bool {
matches!(self, CandidatePreference::Required(_))
}
}
pub const DEFAULT_SKIP: [&str; 3] = ["goatd-primal", "hypergraph-bisect", "guided-bisect"];
impl Default for PortfolioKnobs {
fn default() -> Self {
PortfolioKnobs {
build_history: PortfolioBuildHistory::default(),
seed: 0,
trace: TraceLevel::Off,
flowcutter_cap_ms: None,
peak_tolerance: DEFAULT_PEAK_TOLERANCE,
prefer: None,
skip: DEFAULT_SKIP.to_vec(),
ranker: true,
pairwise_weighting: PairwiseWeighting::default(),
}
}
}
#[derive(Clone, Copy, Debug, Default, PartialEq, Eq)]
pub enum TraceLevel {
#[default]
Off,
Scored,
All,
}
impl PortfolioKnobs {
pub fn candidate_names() -> Vec<String> {
driver::catalog()
.iter()
.flat_map(|c| c.published_specs())
.collect()
}
pub(super) fn with_env_defaults(self) -> Result<Self, crate::error::VitriError> {
use crate::env::{env_raw, parse};
let PortfolioKnobs {
build_history,
seed,
trace,
flowcutter_cap_ms,
peak_tolerance,
prefer,
skip,
ranker,
pairwise_weighting,
} = self;
Ok(PortfolioKnobs {
build_history,
seed: parse(
"VITRI_PORTFOLIO_SEED",
seed,
"a non-negative integer seed for the portfolio's goatd-incidence candidate",
)?,
trace: match env_raw(
"VITRI_PORTFOLIO_TRACE",
"any value to trace every scored candidate, or `all` to also \
build and score the candidates the generation gate skips",
)? {
Some(raw) if crate::env::is_form(&raw, "all") => TraceLevel::All,
Some(_) => TraceLevel::Scored,
None => trace,
},
flowcutter_cap_ms: positive_ms(parse(
"VITRI_PMC_FLOWCUTTER_CAP_MS",
flowcutter_cap_ms.unwrap_or(0),
"a wall-clock cap in milliseconds for the projected FlowCutter \
candidates (0 = no cap)",
)?),
peak_tolerance,
prefer,
skip: match env_raw(
"VITRI_PORTFOLIO_SKIP",
"built-in catalog entry names separated by `;`, left out of the portfolio in \
place of the default list; empty leaves none out",
)? {
Some(raw) => parse_skip_names(&raw)?,
None => skip,
},
ranker,
pairwise_weighting,
})
}
}
fn parse_skip_names(raw: &str) -> Result<Vec<&'static str>, crate::error::VitriError> {
let known: Vec<&'static str> = driver::catalog().iter().map(|c| c.name).collect();
let mut names = Vec::new();
for piece in raw.split(';') {
let piece = piece.trim();
if piece.is_empty() {
continue;
}
let Some(&name) = known.iter().find(|k| **k == piece) else {
return Err(crate::error::VitriError::env(
"VITRI_PORTFOLIO_SKIP",
format!("{piece:?} is not a built-in catalog entry; the entries are {known:?}"),
));
};
if !names.contains(&name) {
names.push(name);
}
}
if known.iter().all(|k| names.contains(k)) {
return Err(crate::error::VitriError::env(
"VITRI_PORTFOLIO_SKIP",
"names every built-in entry, which leaves the portfolio nothing to build",
));
}
Ok(names)
}
#[derive(Clone, Debug, Default, PartialEq)]
pub struct PortfolioBuildHistory {
last_build_ms: std::rc::Rc<std::cell::Cell<u64>>,
last_winning_spec: std::rc::Rc<std::cell::RefCell<Option<String>>>,
last_scores: std::rc::Rc<std::cell::Cell<Option<crate::score::VtreeScores>>>,
}
impl PortfolioBuildHistory {
pub fn last_build_ms(&self) -> Option<u64> {
match self.last_build_ms.get() {
0 => None,
elapsed_ms => Some(elapsed_ms),
}
}
pub fn last_winning_spec(&self) -> Option<String> {
self.last_winning_spec.borrow().clone()
}
pub fn last_scores(&self) -> Option<crate::score::VtreeScores> {
self.last_scores.get()
}
fn record(&self, elapsed_ms: u64) {
self.last_build_ms.set(elapsed_ms);
}
fn record_winner(&self, winning_spec: &str, scores: crate::score::VtreeScores) {
*self.last_winning_spec.borrow_mut() = Some(winning_spec.to_owned());
self.last_scores.set(Some(scores));
}
}
fn positive_ms(ms: i64) -> Option<i64> {
(ms > 0).then_some(ms)
}