use crate::decompose::{
BINARIZATIONS, ClauseWeight, FC_BARE_TIMEOUT_MS, FC_DEFAULT_ITERS, FC_DEFAULT_STEPS_ITERS,
FC_PATIENCE_MS_BARE, FC_PATIENCE_MS_PARAMETRIZED, ForceConfig, ForceMode, InitMode, OrientRule,
PLACES, ROOTS, Reading, RootRule, WeightRule,
};
use crate::error::VitriError;
fn force_dim_range() -> String {
format!("an integer 2..={}", crate::decompose::FORCE_MAX_DIM)
}
fn invalid_token(spec: &str, what: &str, got: &str, expected: &str) -> VitriError {
VitriError::spec(spec, format!("invalid {what} {got:?}, expected {expected}"))
}
fn one_of<T: std::fmt::Display>(names: impl IntoIterator<Item = T>) -> String {
let names: Vec<String> = names.into_iter().map(|n| n.to_string()).collect();
match names.split_last() {
Some((last, [])) => last.clone(),
Some((last, rest)) => format!("{} or {last}", rest.join(", ")),
None => String::new(),
}
}
fn lookup<T: Copy>(table: &[(&'static str, T)], name: &str) -> Option<T> {
table.iter().find(|(n, _)| *n == name).map(|(_, v)| *v)
}
fn value_names<T>(table: &[(&'static str, T)]) -> impl Iterator<Item = &'static str> {
table.iter().map(|(n, _)| *n)
}
const FORCE_TREEIFIERS: &[(&str, ForceMode)] = &[("mst", ForceMode::Mst), ("cut", ForceMode::Cut)];
const FORCE_ROOTS: &[(&str, RootRule)] = &[
("merge", RootRule::Merge),
("balance", RootRule::Balance),
("hybrid", RootRule::Hybrid),
];
const FORCE_ORIENTS: &[(&str, OrientRule)] = &[
("x", OrientRule::X),
("small", OrientRule::Small),
("big", OrientRule::Big),
];
const FORCE_WEIGHTS: &[(&str, WeightRule)] =
&[("euclid", WeightRule::Euclid), ("co", WeightRule::Co)];
const FORCE_CLAUSE_WEIGHTS: &[(&str, ClauseWeight)] = &[
("uniform", ClauseWeight::Uniform),
("short", ClauseWeight::Short),
];
const FORCE_INITS: &[(&str, InitMode)] =
&[("rand", InitMode::Rand), ("force1d", InitMode::Force1d)];
const TIE_BREAKS: &[(&str, bool)] = &[("fixed", false), ("jw-sample", true)];
const REFINEMENTS: &[(&str, bool)] = &[("on", true), ("off", false)];
fn candidate_range() -> String {
format!(
"an integer from 0 to {}",
crate::decompose::MAX_GOATD_CANDIDATES - 1
)
}
#[derive(Clone, Copy, PartialEq, Eq)]
enum BaseGroup {
Decomposition,
Baseline,
Standalone,
}
struct VtreeBaseName {
name: &'static str,
family: VtreeBase,
}
impl VtreeBaseName {
const fn new(name: &'static str, family: VtreeBase) -> Self {
Self { name, family }
}
}
pub(crate) const BALANCED_SPEC: &str = "balanced";
const VTREE_BASE_NAMES: &[VtreeBaseName] = &[
VtreeBaseName::new(BALANCED_SPEC, VtreeBase::Balanced),
VtreeBaseName::new("linear", VtreeBase::Linear),
VtreeBaseName::new("reverse-linear", VtreeBase::ReverseLinear),
VtreeBaseName::new("random", VtreeBase::Random),
VtreeBaseName::new("portfolio", VtreeBase::Portfolio),
VtreeBaseName::new(
"flowcutter-primal",
VtreeBase::Flowcutter { incidence: false },
),
VtreeBaseName::new(
"flowcutter-incidence",
VtreeBase::Flowcutter { incidence: true },
),
VtreeBaseName::new("goatd-primal", VtreeBase::Goatd { incidence: false }),
VtreeBaseName::new("goatd-incidence", VtreeBase::Goatd { incidence: true }),
VtreeBaseName::new("guided-bisect", VtreeBase::GuidedBisect),
VtreeBaseName::new("hypergraph-bisect", VtreeBase::HypergraphBisect),
VtreeBaseName::new("primal-bisect", VtreeBase::PrimalBisect),
VtreeBaseName::new("force", VtreeBase::Force),
];
pub(crate) fn decomposition_spec_names() -> impl Iterator<Item = &'static str> {
base_names(BaseGroup::Decomposition)
}
pub(crate) fn baseline_spec_names() -> impl Iterator<Item = &'static str> {
base_names(BaseGroup::Baseline)
}
pub(crate) fn standalone_spec_names() -> impl Iterator<Item = &'static str> {
base_names(BaseGroup::Standalone)
}
pub fn vtree_spec_bases() -> Vec<String> {
let mut names: Vec<String> = VTREE_BASE_NAMES
.iter()
.map(|b| b.name.to_string())
.collect();
for name in crate::decompose::elimination_spec_names() {
for (suffix, _) in crate::decompose::VIEW_SUFFIXES {
names.push(format!("{name}{suffix}"));
}
}
names
}
fn base_names(group: BaseGroup) -> impl Iterator<Item = &'static str> {
VTREE_BASE_NAMES
.iter()
.filter(move |b| help_group(b.family) == Some(group))
.map(|b| b.name)
}
fn help_group(family: VtreeBase) -> Option<BaseGroup> {
Some(match family {
VtreeBase::Balanced | VtreeBase::Linear | VtreeBase::ReverseLinear | VtreeBase::Random => {
BaseGroup::Baseline
}
VtreeBase::Portfolio | VtreeBase::Force => BaseGroup::Standalone,
VtreeBase::Goatd { .. }
| VtreeBase::Flowcutter { .. }
| VtreeBase::GuidedBisect
| VtreeBase::HypergraphBisect
| VtreeBase::PrimalBisect => BaseGroup::Decomposition,
VtreeBase::Elimination { .. } | VtreeBase::Unknown => return None,
})
}
#[derive(Clone, Copy, PartialEq, Eq, Debug)]
pub(crate) enum VtreeBase {
Balanced,
Linear,
ReverseLinear,
Random,
Portfolio,
Goatd {
incidence: bool,
},
Elimination {
name: &'static str,
incidence: bool,
},
Flowcutter {
incidence: bool,
},
GuidedBisect,
HypergraphBisect,
PrimalBisect,
Force,
Unknown,
}
impl VtreeBase {
pub(crate) fn is_structural(self) -> bool {
help_group(self) != Some(BaseGroup::Baseline)
}
}
pub(crate) fn classify_base(base: &str) -> VtreeBase {
let named = VTREE_BASE_NAMES.iter().find(|b| match b.family {
VtreeBase::Random => base.starts_with(b.name),
_ => base == b.name,
});
if let Some(b) = named {
return b.family;
}
match crate::decompose::elimination_spec(base) {
Some((name, incidence)) => VtreeBase::Elimination { name, incidence },
None => VtreeBase::Unknown,
}
}
pub(crate) fn spec_has_candidates(spec: &str) -> bool {
matches!(classify_base(vtree_spec_base(spec)), VtreeBase::Portfolio)
}
fn split_vtree_spec(spec: &str) -> (&str, Option<&str>) {
match spec.split_once(':') {
Some((b, p)) => (b, Some(p)),
None => (spec, None),
}
}
pub(crate) fn vtree_spec_base(spec: &str) -> &str {
split_vtree_spec(spec).0
}
struct SpecParamKey {
key: &'static str,
accepts: fn(VtreeBase) -> bool,
values: fn() -> String,
default: &'static str,
what: &'static str,
}
const SPEC_PARAM_KEYS: &[SpecParamKey] = &[
SpecParamKey {
key: "seed",
accepts: |f| matches!(f, VtreeBase::Goatd { .. } | VtreeBase::Elimination { .. }),
values: || "an integer".to_string(),
default: "0",
what: "which random tie-break the elimination takes",
},
SpecParamKey {
key: "ties",
accepts: |f| {
matches!(f, VtreeBase::Elimination { name, .. }
if crate::decompose::elimination_order_samples(name))
},
values: || one_of(value_names(TIE_BREAKS)),
default: "fixed",
what: "how the elimination breaks a tie between two candidate variables",
},
SpecParamKey {
key: "refine",
accepts: |f| matches!(f, VtreeBase::Goatd { .. }),
values: || one_of(value_names(REFINEMENTS)),
default: "on",
what: "whether the schedule ends in the refinement pass, or runs one \
unrefined elimination slot",
},
SpecParamKey {
key: "candidate",
accepts: |f| matches!(f, VtreeBase::Goatd { .. }),
values: candidate_range,
default: "0",
what: "which of the refined schedule's decompositions becomes the tree: 0 \
the winner, refined; n above 0 its nth runner-up, unrefined",
},
SpecParamKey {
key: "imbalance",
accepts: |f| matches!(f, VtreeBase::HypergraphBisect | VtreeBase::PrimalBisect),
values: || "a fraction in 0.0..=0.5".to_string(),
default: "0.03",
what: "how far either side may deviate from an even split",
},
SpecParamKey {
key: "budget",
accepts: fc_family,
values: || "<N>ms (timed) or <N>steps (step-budgeted)".to_string(),
default: "200ms",
what: "how hard FlowCutter looks for a decomposition",
},
SpecParamKey {
key: "iters",
accepts: fc_family,
values: || "an integer".to_string(),
default: "100000 timed, 900 step-budgeted",
what: "how many FlowCutter iterations the search runs",
},
SpecParamKey {
key: "patience",
accepts: fc_family,
values: || "milliseconds without an improvement before the search stops".to_string(),
default: "100 with no budget written, 150 with one",
what: "how long the timed search waits for an improvement",
},
SpecParamKey {
key: "root",
accepts: conversion_family,
values: || one_of(value_names(ROOTS)),
default: "searched",
what: "which bag the decomposition is rooted at",
},
SpecParamKey {
key: "place",
accepts: conversion_family,
values: || one_of(value_names(PLACES)),
default: "searched",
what: "which bag of the decomposition each variable is placed in",
},
SpecParamKey {
key: "binarize",
accepts: conversion_family,
values: || one_of(value_names(BINARIZATIONS)),
default: "searched",
what: "how each bag's children and variable leaves are binarized",
},
SpecParamKey {
key: "treeify",
accepts: is_force,
values: || one_of(value_names(FORCE_TREEIFIERS)),
default: "mst",
what: "which tree-ifier turns the embedding into a vtree",
},
SpecParamKey {
key: "root",
accepts: is_force,
values: || one_of(value_names(FORCE_ROOTS)),
default: "merge",
what: "where the MST is rooted",
},
SpecParamKey {
key: "orient",
accepts: is_force,
values: || one_of(value_names(FORCE_ORIENTS)),
default: "x",
what: "how an MST edge becomes a left/right child pair",
},
SpecParamKey {
key: "weights",
accepts: is_force,
values: || one_of(value_names(FORCE_WEIGHTS)),
default: "euclid",
what: "what an MST edge weighs",
},
SpecParamKey {
key: "feedback",
accepts: is_force,
values: || "an integer 0..=8".to_string(),
default: "0",
what: "how many feedback rounds reshape the layout",
},
SpecParamKey {
key: "clause-weight",
accepts: is_force,
values: || one_of(value_names(FORCE_CLAUSE_WEIGHTS)),
default: "uniform",
what: "how strongly a clause pulls its variables together",
},
SpecParamKey {
key: "dim",
accepts: is_force,
values: || force_dim_range(),
default: "2",
what: "how many dimensions the variables are embedded in",
},
SpecParamKey {
key: "restarts",
accepts: is_force,
values: || "an integer 1..=16".to_string(),
default: "1",
what: "how many layouts are tried, keeping the best",
},
SpecParamKey {
key: "init",
accepts: is_force,
values: || one_of(value_names(FORCE_INITS)),
default: "rand",
what: "how the layout starts",
},
];
fn conversion_family(family: VtreeBase) -> bool {
matches!(
family,
VtreeBase::Flowcutter { .. } | VtreeBase::Goatd { .. } | VtreeBase::Elimination { .. }
)
}
fn fc_family(family: VtreeBase) -> bool {
matches!(
family,
VtreeBase::Flowcutter { .. } | VtreeBase::GuidedBisect
)
}
fn is_force(family: VtreeBase) -> bool {
matches!(family, VtreeBase::Force)
}
const FORCE_MST_ONLY_KEYS: &[&str] = &["root", "orient", "weights", "feedback"];
fn read_reading(params: &mut KeyedParams<'_>) -> Result<Reading, VitriError> {
Ok(Reading {
root: params.enum_value("root", ROOTS)?,
place: params.enum_value("place", PLACES)?,
binarize: params.enum_value("binarize", BINARIZATIONS)?,
})
}
fn keys_for(family: VtreeBase) -> Vec<String> {
SPEC_PARAM_KEYS
.iter()
.filter(|k| (k.accepts)(family))
.map(|k| format!("{}=", k.key))
.collect()
}
pub struct SpecParamDoc {
pub key: &'static str,
pub values: String,
pub default: &'static str,
pub what: &'static str,
}
pub fn spec_param_docs(spec_base: &str) -> Vec<SpecParamDoc> {
let family = classify_base(spec_base);
SPEC_PARAM_KEYS
.iter()
.filter(|k| (k.accepts)(family))
.map(|k| SpecParamDoc {
key: k.key,
values: (k.values)(),
default: k.default,
what: k.what,
})
.collect()
}
pub(crate) fn spec_string(base: &str, params: Option<&str>) -> String {
match params {
Some(p) if !p.is_empty() => format!("{base}:{p}"),
_ => base.to_string(),
}
}
struct KeyedParams<'a> {
spec: &'a str,
entries: Vec<Entry<'a>>,
}
struct Entry<'a> {
key: &'a str,
value: &'a str,
used: bool,
}
impl<'a> KeyedParams<'a> {
fn new(spec: &'a str, raw: Option<&'a str>) -> Result<Self, VitriError> {
let mut entries: Vec<Entry<'a>> = Vec::new();
for part in raw.into_iter().flat_map(|p| p.split(',')) {
let Some((key, value)) = part.split_once('=') else {
return Err(VitriError::spec(
spec,
format!("parameter {part:?} must be written key=value"),
));
};
if key.is_empty() {
return Err(VitriError::spec(
spec,
format!("parameter {part:?} has an empty key"),
));
}
if entries.iter().any(|e| e.key == key) {
return Err(VitriError::spec(
spec,
format!(
"parameter {key:?} is written twice; one of the two values would be \
dropped. Write it once"
),
));
}
entries.push(Entry {
key,
value,
used: false,
});
}
Ok(KeyedParams { spec, entries })
}
fn written(&self) -> Vec<(&'a str, &'a str)> {
let rank = |key: &str| {
SPEC_PARAM_KEYS
.iter()
.position(|k| k.key == key)
.unwrap_or(usize::MAX)
};
let mut pairs: Vec<(&'a str, &'a str)> =
self.entries.iter().map(|e| (e.key, e.value)).collect();
pairs.sort_by_key(|(key, _)| rank(key));
pairs
}
fn take(&mut self, key: &str) -> Option<&'a str> {
self.entries.iter_mut().find(|e| e.key == key).map(|e| {
e.used = true;
e.value
})
}
fn wrote(&self, key: &str) -> bool {
self.entries.iter().any(|e| e.key == key)
}
fn enum_value<T: Copy>(
&mut self,
key: &str,
table: &[(&'static str, T)],
) -> Result<Option<T>, VitriError> {
match self.take(key) {
None => Ok(None),
Some(v) => match lookup(table, v) {
Some(found) => Ok(Some(found)),
None => Err(invalid_token(
self.spec,
key,
v,
&one_of(value_names(table)),
)),
},
}
}
fn number<T: std::str::FromStr>(
&mut self,
key: &str,
expected: &str,
) -> Result<Option<T>, VitriError> {
match self.take(key) {
None => Ok(None),
Some(v) => match v.parse::<T>() {
Ok(n) => Ok(Some(n)),
Err(_) => Err(invalid_token(self.spec, key, v, expected)),
},
}
}
fn finish(&self, family: VtreeBase, base: &str) -> Result<(), VitriError> {
let Some(unread) = self.entries.iter().find(|e| !e.used) else {
return Ok(());
};
let accepted = keys_for(family);
let offer = if accepted.is_empty() {
format!("{base:?} takes no parameters")
} else {
format!("{base:?} takes {}", one_of(accepted))
};
Err(VitriError::spec(
self.spec,
format!("parameter \"{}=\" is not one {offer}", unread.key),
))
}
}
pub(crate) struct ParsedSpec<'a> {
pub raw: &'a str,
pub base: &'a str,
pub family: VtreeBase,
pub param: SpecParam,
pub reading: Reading,
written: Vec<(&'a str, &'a str)>,
}
impl std::fmt::Display for ParsedSpec<'_> {
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
let params: Vec<String> = self
.written
.iter()
.map(|(key, value)| format!("{key}={value}"))
.collect();
f.write_str(&spec_string(self.base, Some(¶ms.join(","))))
}
}
impl ParsedSpec<'_> {
pub(crate) fn inherit(&mut self, base: Reading) {
self.reading = self.reading.inherit(base);
}
}
pub(crate) enum SpecParam {
None,
Elimination {
jw_sample: bool,
seed: u64,
},
Goatd {
refine: bool,
seed: u64,
candidate: u32,
},
Imbalance(f64),
FcTimed {
timeout_ms: i64,
iters: i32,
patience_ms: i64,
},
FcSteps {
steps: i64,
iters: i32,
},
Force(crate::decompose::ForceConfig),
}
impl SpecParam {
pub(crate) fn seed(&self) -> u64 {
match *self {
SpecParam::Elimination { seed, .. } | SpecParam::Goatd { seed, .. } => seed,
_ => 0,
}
}
pub(crate) fn jw_sample(&self) -> bool {
matches!(
*self,
SpecParam::Elimination {
jw_sample: true,
..
}
)
}
pub(crate) fn refine(&self) -> bool {
!matches!(*self, SpecParam::Goatd { refine: false, .. })
}
pub(crate) fn candidate(&self) -> u32 {
match *self {
SpecParam::Goatd { candidate, .. } => candidate,
_ => 0,
}
}
pub(crate) fn imbalance(&self) -> f64 {
match *self {
SpecParam::Imbalance(v) => v,
_ => crate::decompose::IMBALANCE_BALANCED,
}
}
pub(crate) fn force(&self) -> crate::decompose::ForceConfig {
match *self {
SpecParam::Force(cfg) => cfg,
_ => crate::decompose::ForceConfig::new(crate::decompose::ForceMode::Mst),
}
}
pub(crate) fn fc_budget(&self, base: &str) -> Result<crate::decompose::FcBudget, VitriError> {
match *self {
SpecParam::FcTimed {
timeout_ms,
iters,
patience_ms,
} => Ok(crate::decompose::FcBudget::timed(
timeout_ms,
patience_ms,
iters,
)),
SpecParam::FcSteps { steps, iters } => {
Ok(crate::decompose::FcBudget::Steps { steps, iters })
}
_ => Err(VitriError::spec(
base,
"no FlowCutter budget, expected \"budget=<N>ms\" or \"budget=<N>steps\"",
)),
}
}
}
pub(crate) fn parse_vtree_spec(spec: &str) -> Result<ParsedSpec<'_>, VitriError> {
let (base, raw_params) = split_vtree_spec(spec);
let family = classify_base(base);
let mut params = KeyedParams::new(spec, raw_params)?;
let written = params.written();
let mut reading = Reading::default();
let param = match family {
VtreeBase::Balanced
| VtreeBase::Linear
| VtreeBase::ReverseLinear
| VtreeBase::Random
| VtreeBase::Portfolio => SpecParam::None,
VtreeBase::Goatd { .. } => {
reading = read_reading(&mut params)?;
let refine = params.enum_value("refine", REFINEMENTS)?.unwrap_or(true);
let candidate = params.number("candidate", &candidate_range())?.unwrap_or(0);
if candidate >= crate::decompose::MAX_GOATD_CANDIDATES {
return Err(invalid_token(
spec,
"candidate",
&candidate.to_string(),
&candidate_range(),
));
}
if candidate > 0 && !refine {
return Err(VitriError::spec(
spec,
"\"candidate=\" names a runner-up of the refined schedule and has \
nothing to name under \"refine=off\"",
));
}
SpecParam::Goatd {
refine,
seed: params.number("seed", "an integer")?.unwrap_or(0),
candidate,
}
}
VtreeBase::Elimination { name, .. } => {
let jw_sample = crate::decompose::elimination_order_samples(name)
&& params.enum_value("ties", TIE_BREAKS)?.unwrap_or(false);
let seed = params.number("seed", "an integer")?.unwrap_or(0);
reading = read_reading(&mut params)?;
SpecParam::Elimination { jw_sample, seed }
}
VtreeBase::Flowcutter { .. } => {
let budget = parse_fc_budget(&mut params, spec)?;
reading = read_reading(&mut params)?;
budget
}
VtreeBase::GuidedBisect => parse_fc_budget(&mut params, spec)?,
VtreeBase::HypergraphBisect | VtreeBase::PrimalBisect => {
let v: f64 = params
.number("imbalance", "a fraction in 0.0..=0.5")?
.unwrap_or(crate::decompose::IMBALANCE_BALANCED);
if !(0.0..=0.5).contains(&v) {
return Err(invalid_token(
spec,
"imbalance",
&v.to_string(),
"a finite fraction in 0.0..=0.5",
));
}
SpecParam::Imbalance(v)
}
VtreeBase::Force => SpecParam::Force(parse_force_config(&mut params, spec)?),
VtreeBase::Unknown => {
return Ok(ParsedSpec {
raw: spec,
base,
family,
param: SpecParam::None,
reading,
written,
});
}
};
params.finish(family, base)?;
Ok(ParsedSpec {
raw: spec,
base,
family,
param,
reading,
written,
})
}
pub fn validate_vtree_spec(spec: &str) -> Result<(), VitriError> {
let parsed = parse_vtree_spec(spec)?;
if matches!(parsed.family, VtreeBase::Unknown) {
return Err(unknown_vtree_type(spec));
}
Ok(())
}
fn parse_fc_budget(params: &mut KeyedParams<'_>, spec: &str) -> Result<SpecParam, VitriError> {
let written = params.take("budget");
let iters_key = "iters";
match written {
Some(v) if v.ends_with("steps") => {
let steps: i64 = v
.trim_end_matches("steps")
.parse()
.map_err(|_| invalid_token(spec, "budget", v, "<N>steps"))?;
let iters = params
.number(iters_key, "an integer")?
.unwrap_or(FC_DEFAULT_STEPS_ITERS);
if params.wrote("patience") {
return Err(VitriError::spec(
spec,
"\"patience=\" bounds the timed search and has nothing to bound in the \
step-budgeted \"budget=<N>steps\" mode",
));
}
Ok(SpecParam::FcSteps { steps, iters })
}
Some(v) => {
let timeout_ms: i64 = v
.trim_end_matches("ms")
.parse()
.map_err(|_| invalid_token(spec, "budget", v, "<N>ms or <N>steps"))?;
if !v.ends_with("ms") {
return Err(invalid_token(spec, "budget", v, "<N>ms or <N>steps"));
}
Ok(SpecParam::FcTimed {
timeout_ms,
iters: params
.number(iters_key, "an integer")?
.unwrap_or(FC_DEFAULT_ITERS),
patience_ms: params
.number("patience", "milliseconds")?
.unwrap_or(FC_PATIENCE_MS_PARAMETRIZED),
})
}
None => Ok(SpecParam::FcTimed {
timeout_ms: FC_BARE_TIMEOUT_MS,
iters: params
.number(iters_key, "an integer")?
.unwrap_or(FC_DEFAULT_ITERS),
patience_ms: params
.number("patience", "milliseconds")?
.unwrap_or(FC_PATIENCE_MS_BARE),
}),
}
}
fn parse_force_config(params: &mut KeyedParams<'_>, spec: &str) -> Result<ForceConfig, VitriError> {
let mode = params
.enum_value("treeify", FORCE_TREEIFIERS)?
.unwrap_or(ForceMode::Mst);
if mode != ForceMode::Mst
&& let Some(key) = FORCE_MST_ONLY_KEYS.iter().find(|k| params.wrote(k))
{
return Err(VitriError::spec(
spec,
format!(
"\"{key}=\" reshapes the MST and cannot combine with \"treeify=cut\", which \
selects the median-cut tree-ifier and has no MST to reshape"
),
));
}
let mut cfg = ForceConfig::new(mode);
if let Some(v) = params.enum_value("root", FORCE_ROOTS)? {
cfg.root = v;
}
if let Some(v) = params.enum_value("orient", FORCE_ORIENTS)? {
cfg.orient = v;
}
if let Some(v) = params.enum_value("weights", FORCE_WEIGHTS)? {
cfg.weight = v;
}
if let Some(v) = params.enum_value("clause-weight", FORCE_CLAUSE_WEIGHTS)? {
cfg.clause_weight = v;
}
if let Some(v) = params.enum_value("init", FORCE_INITS)? {
cfg.init = v;
}
if let Some(v) = params.number::<usize>("dim", &force_dim_range())? {
if !(2..=crate::decompose::FORCE_MAX_DIM).contains(&v) {
return Err(invalid_token(
spec,
"dim",
&v.to_string(),
&force_dim_range(),
));
}
cfg.dim = v;
}
if let Some(v) = params.number::<u8>("feedback", "an integer 0..=8")? {
if v > 8 {
return Err(invalid_token(
spec,
"feedback",
&v.to_string(),
"an integer 0..=8",
));
}
cfg.fb = v;
}
if let Some(v) = params.number::<u8>("restarts", "an integer 1..=16")? {
if !(1..=16).contains(&v) {
return Err(invalid_token(
spec,
"restarts",
&v.to_string(),
"an integer 1..=16",
));
}
cfg.seeds = v;
}
Ok(cfg)
}
pub(super) fn unknown_vtree_type(spec: &str) -> VitriError {
let bases: Vec<&str> = VTREE_BASE_NAMES.iter().map(|b| b.name).collect();
let orders: Vec<&str> = crate::decompose::elimination_spec_names().collect();
VitriError::spec(
spec,
format!(
"unknown vtree type, expected {}, or one of the elimination orders {}, each \
written with the graph view it runs on ({})",
one_of(bases),
one_of(orders),
one_of(
crate::decompose::VIEW_SUFFIXES
.iter()
.map(|(suffix, _)| format!("\"<order>{suffix}\""))
),
),
)
}