use crate::construct::{
AddelmanKempthorne, Bose, BoseBush, Bush, Constructor, HadamardSylvester, RaoHamming,
};
use crate::error::{Error, Result};
use crate::oa::OA;
use crate::utils::factor_prime_power;
#[derive(Debug, Clone, Default)]
pub struct OABuilder {
levels: Option<Vec<u32>>,
factors: Option<usize>,
strength: Option<u32>,
min_runs: Option<usize>,
}
impl OABuilder {
#[must_use]
pub fn new() -> Self {
Self::default()
}
#[must_use]
pub fn levels(mut self, levels: u32) -> Self {
self.levels = Some(vec![levels]);
self
}
#[must_use]
pub fn mixed_levels(mut self, levels: Vec<u32>) -> Self {
self.levels = Some(levels);
self.factors = Some(self.levels.as_ref().unwrap().len());
self
}
#[must_use]
pub fn factors(mut self, factors: usize) -> Self {
self.factors = Some(factors);
if let Some(ref mut lv) = self.levels {
if lv.len() == 1 && factors > 1 {
let s = lv[0];
*lv = vec![s; factors];
}
}
self
}
#[must_use]
pub fn strength(mut self, strength: u32) -> Self {
self.strength = Some(strength);
self
}
#[must_use]
pub fn min_runs(mut self, min_runs: usize) -> Self {
self.min_runs = Some(min_runs);
self
}
pub fn build(self) -> Result<OA> {
let levels_vec = self
.levels
.clone()
.ok_or_else(|| Error::invalid_params("levels must be specified"))?;
let factors = self
.factors
.ok_or_else(|| Error::invalid_params("factors must be specified"))?;
let strength = self.strength.unwrap_or(2);
let min_runs = self.min_runs.unwrap_or(0);
if levels_vec.is_empty() {
return Err(Error::invalid_params("levels must not be empty"));
}
if factors == 0 {
return Err(Error::invalid_params("factors must be at least 1"));
}
if strength == 0 {
return Err(Error::invalid_params("strength must be at least 1"));
}
if strength as usize > factors {
return Err(Error::invalid_params(
"strength cannot exceed number of factors",
));
}
let is_symmetric = levels_vec.len() == 1 || levels_vec.iter().all(|&s| s == levels_vec[0]);
if is_symmetric {
let levels = levels_vec[0];
return self.auto_select(levels, factors, strength, min_runs);
}
let max_s = *levels_vec.iter().max().unwrap();
for q in max_s..=256 {
if crate::utils::is_prime_power(q) && levels_vec.iter().all(|&s| q % s == 0) {
if let Ok(base_oa) = self.auto_select(q, factors, strength, min_runs) {
let mut mixed_oa = base_oa;
for (i, &s) in levels_vec.iter().enumerate() {
if s < q {
mixed_oa = mixed_oa.collapse_levels(i, s)?;
}
}
return Ok(mixed_oa);
}
}
}
Err(Error::invalid_params(format!(
"No construction available for mixed-level OA with levels {:?}. \
Try different parameters or a smaller number of factors.",
levels_vec
)))
}
fn auto_select(
&self,
levels: u32,
factors: usize,
strength: u32,
min_runs: usize,
) -> Result<OA> {
let prime_power = factor_prime_power(levels);
if levels == 2 && strength == 2 {
let mut n = 4;
while n - 1 < factors || n < min_runs {
n *= 2;
if n > 1 << 20 {
break; }
}
if n - 1 >= factors && n >= min_runs {
if let Ok(h) = HadamardSylvester::new(n) {
if let Ok(oa) = h.construct(factors) {
return Ok(oa);
}
}
}
}
if strength == 2 {
if levels == 2 {
let bb_runs = 8; let bb_max_factors = 5;
if factors <= bb_max_factors && bb_runs >= min_runs {
if let Ok(bb) = BoseBush::new(2) {
if let Ok(oa) = bb.construct(factors) {
return Ok(oa);
}
}
}
}
if let Some(ref _pf) = prime_power {
let q = levels;
let bose_max_factors = (q + 1) as usize;
let bose_runs = (q * q) as usize;
if factors <= bose_max_factors && bose_runs >= min_runs {
let bose = Bose::new(q);
if let Ok(oa) = bose.construct(factors) {
return Ok(oa);
}
}
}
if let Some(ref pf) = prime_power {
if pf.prime != 2 {
let q = levels;
let ak_max_factors = (2 * q + 1) as usize;
let ak_runs = (2 * q * q) as usize;
if factors <= ak_max_factors && ak_runs >= min_runs {
if let Ok(ak) = AddelmanKempthorne::new(q) {
if let Ok(oa) = ak.construct(factors) {
return Ok(oa);
}
}
}
}
}
if let Some(ref _pf) = prime_power {
let q = levels;
for m in 2..=10 {
let rh_runs = (q as usize).pow(m);
let rh_max_factors = (rh_runs - 1) / (q as usize - 1);
if factors <= rh_max_factors && rh_runs >= min_runs {
if let Ok(rh) = RaoHamming::new(q, m) {
if let Ok(oa) = rh.construct(factors) {
return Ok(oa);
}
}
}
if rh_runs > 1024 && rh_runs > min_runs {
break;
}
}
}
}
if let Some(ref _pf) = prime_power {
let q = levels;
let t = strength;
let bush_max_factors = (t + 1) as usize;
let bush_runs = q.pow(t) as usize;
if factors <= bush_max_factors && bush_runs >= min_runs {
if let Ok(bush) = Bush::new(q, t) {
if let Ok(oa) = bush.construct(factors) {
return Ok(oa);
}
}
}
}
Err(Error::invalid_params(format!(
"No construction available for OA(?, {}, {}, {}). \
Try different parameters or a smaller number of factors.",
factors, levels, strength
)))
}
}
pub fn build_oa(levels: u32, factors: usize, strength: u32) -> Result<OA> {
OABuilder::new()
.levels(levels)
.factors(factors)
.strength(strength)
.build()
}
pub fn available_constructions(levels: u32, strength: u32) -> Vec<(&'static str, usize, usize)> {
let mut options = Vec::new();
let prime_power = factor_prime_power(levels);
if levels == 2 && strength == 2 {
for m in 2..=10 {
let n = 1 << m;
options.push(("HadamardSylvester", n, n - 1));
}
}
if levels == 2 && strength == 2 {
for &p in &[3u32, 7, 11, 19, 23, 31, 43, 47, 59, 67, 71, 79, 83] {
options.push(("HadamardPaley", (p + 1) as usize, p as usize));
}
}
if levels == 2 && strength == 2 {
options.push(("BoseBush", 8, 5));
}
if strength == 2 {
if let Some(ref _pf) = prime_power {
let q = levels;
options.push(("Bose", (q * q) as usize, (q + 1) as usize));
}
}
if strength == 2 {
if let Some(ref pf) = prime_power {
if pf.prime != 2 {
let q = levels;
options.push((
"AddelmanKempthorne",
(2 * q * q) as usize,
(2 * q + 1) as usize,
));
}
}
}
if strength == 2 {
if let Some(ref _pf) = prime_power {
let q = levels;
for m in 2..=5 {
let n = (q as usize).pow(m);
let k = (n - 1) / (q as usize - 1);
options.push(("RaoHamming", n, k));
}
}
}
if let Some(ref _pf) = prime_power {
let q = levels;
let t = strength;
options.push(("Bush", q.pow(t) as usize, (t + 1) as usize));
}
options
}
#[cfg(test)]
mod tests {
use super::*;
use crate::oa::verify_strength;
#[test]
fn test_builder_basic() {
let oa = OABuilder::new()
.levels(3)
.factors(4)
.strength(2)
.build()
.unwrap();
assert_eq!(oa.levels(), 3);
assert_eq!(oa.factors(), 4);
assert_eq!(oa.runs(), 9);
let result = verify_strength(&oa, 2).unwrap();
assert!(result.is_valid);
}
#[test]
fn test_builder_binary_many_factors() {
let oa = OABuilder::new()
.levels(2)
.factors(7)
.strength(2)
.build()
.unwrap();
assert_eq!(oa.levels(), 2);
assert_eq!(oa.factors(), 7);
assert_eq!(oa.runs(), 8);
let result = verify_strength(&oa, 2).unwrap();
assert!(result.is_valid);
}
#[test]
fn test_builder_binary_few_factors() {
let oa = OABuilder::new()
.levels(2)
.factors(3)
.strength(2)
.build()
.unwrap();
assert_eq!(oa.levels(), 2);
assert_eq!(oa.factors(), 3);
let result = verify_strength(&oa, 2).unwrap();
assert!(result.is_valid);
}
#[test]
fn test_builder_strength_default() {
let oa = OABuilder::new().levels(3).factors(3).build().unwrap();
assert_eq!(oa.strength(), 2);
}
#[test]
fn test_builder_min_runs() {
let oa = OABuilder::new()
.levels(2)
.factors(3)
.strength(2)
.min_runs(16)
.build()
.unwrap();
assert!(oa.runs() >= 16);
}
#[test]
fn test_builder_missing_levels() {
let result = OABuilder::new().factors(4).build();
assert!(result.is_err());
}
#[test]
fn test_builder_missing_factors() {
let result = OABuilder::new().levels(3).build();
assert!(result.is_err());
}
#[test]
fn test_builder_invalid_strength() {
let result = OABuilder::new()
.levels(3)
.factors(2)
.strength(3) .build();
assert!(result.is_err());
}
#[test]
fn test_build_oa_convenience() {
let oa = build_oa(5, 4, 2).unwrap();
assert_eq!(oa.levels(), 5);
assert_eq!(oa.factors(), 4);
assert_eq!(oa.runs(), 25);
let result = verify_strength(&oa, 2).unwrap();
assert!(result.is_valid);
}
#[test]
fn test_builder_high_strength() {
let oa = OABuilder::new()
.levels(3)
.factors(4)
.strength(3)
.build()
.unwrap();
assert_eq!(oa.levels(), 3);
assert_eq!(oa.factors(), 4);
assert_eq!(oa.strength(), 3);
assert_eq!(oa.runs(), 27);
let result = verify_strength(&oa, 3).unwrap();
assert!(result.is_valid);
}
#[test]
fn test_builder_various_prime_powers() {
for q in [2, 3, 4, 5, 7, 8, 9] {
let oa = OABuilder::new()
.levels(q)
.factors(3)
.strength(2)
.build()
.unwrap();
assert_eq!(oa.levels(), q);
let result = verify_strength(&oa, 2).unwrap();
assert!(result.is_valid, "OA with {} levels should be valid", q);
}
}
#[test]
fn test_builder_large_binary() {
let oa = OABuilder::new()
.levels(2)
.factors(15)
.strength(2)
.build()
.unwrap();
assert_eq!(oa.symmetric_levels(), 2);
assert_eq!(oa.factors(), 15);
assert_eq!(oa.runs(), 16);
let result = verify_strength(&oa, 2).unwrap();
assert!(result.is_valid);
}
#[test]
fn test_builder_mixed_levels() {
let oa = OABuilder::new()
.mixed_levels(vec![2, 2, 2, 4])
.strength(2)
.build()
.unwrap();
assert_eq!(oa.runs(), 16);
assert_eq!(oa.factors(), 4);
assert_eq!(oa.levels_vec(), &[2, 2, 2, 4]);
let result = verify_strength(&oa, 2).unwrap();
assert!(
result.is_valid,
"Mixed OA should be valid: {:?}",
result.issues
);
}
#[test]
fn test_available_constructions() {
let options = available_constructions(3, 2);
let names: Vec<_> = options.iter().map(|(name, _, _)| *name).collect();
assert!(names.contains(&"Bose"));
assert!(names.contains(&"Bush"));
assert!(names.contains(&"RaoHamming"));
}
#[test]
fn test_available_constructions_binary() {
let options = available_constructions(2, 2);
let names: Vec<_> = options.iter().map(|(name, _, _)| *name).collect();
assert!(names.contains(&"HadamardSylvester"));
assert!(names.contains(&"BoseBush"));
assert!(names.contains(&"Bose"));
assert!(names.contains(&"RaoHamming"));
}
}