///|
/// A named factor for a laboratory or process experiment.
pub(all) struct FactorDefinition {
name : String
low : Double
high : Double
center : Double
} derive(Debug, Eq, ToJson)
///|
pub fn FactorDefinition::new(
name~ : String,
low~ : Double,
high~ : Double,
) -> FactorDefinition raise @domain.ExtractionError {
if name.trim().length() == 0 {
raise @domain.ExtractionError::InvalidArgument(
message="factor name cannot be blank",
)
}
@domain.require_finite("factor low", low)
@domain.require_finite("factor high", high)
if high <= low {
raise @domain.ExtractionError::InvalidArgument(
message="factor bounds must increase",
)
}
{ name, low, high, center: (low + high) / 2.0 }
}
///|
pub fn FactorDefinition::scale(
self : FactorDefinition,
coded : Double,
) -> Double {
self.center + coded * (self.high - self.low) / 2.0
}
///|
pub fn FactorDefinition::code(
self : FactorDefinition,
actual : Double,
) -> Double {
2.0 * (actual - self.center) / (self.high - self.low)
}
///|
pub(all) struct ExperimentRun {
run_number : Int
block : Int
coded_values : Array[Double]
actual_values : Array[Double]
center_point : Bool
response : Double?
} derive(Debug, Eq, ToJson)
///|
pub fn full_factorial(
factors : ArrayView[FactorDefinition],
center_points? : Int = 0,
) -> Array[ExperimentRun] raise @domain.ExtractionError {
if factors.length() == 0 {
raise @domain.ExtractionError::InsufficientData(required=1, actual=0)
}
if center_points < 0 {
raise @domain.ExtractionError::InvalidArgument(
message="center point count cannot be negative",
)
}
let coded : Array[Array[Double]] = [[]]
for _ in factors {
let next : Array[Array[Double]] = []
for prefix in coded {
for level in [-1.0, 1.0] {
let value = prefix.copy()
value.push(level)
next.push(value)
}
}
coded.clear()
for value in next {
coded.push(value)
}
}
let runs : Array[ExperimentRun] = []
let mut run_number = 1
for values in coded {
runs.push({
run_number,
block: 1,
coded_values: values.copy(),
actual_values: [
for index, value in values => factors[index].scale(value)
],
center_point: false,
response: None,
})
run_number += 1
}
for _ in 0.. 0.0
],
actual_values: [
for factor in factors => factor.center
],
center_point: true,
response: None,
})
run_number += 1
}
runs
}
///|
pub fn assign_blocks(
runs : ArrayView[ExperimentRun],
block_count~ : Int,
) -> Array[ExperimentRun] raise @domain.ExtractionError {
if block_count < 1 {
raise @domain.ExtractionError::InvalidArgument(
message="block count must be positive",
)
}
[
for index, run in runs => {
{
run_number: run.run_number,
block: index % block_count + 1,
coded_values: run.coded_values,
actual_values: run.actual_values,
center_point: run.center_point,
response: run.response,
}
}
]
}
///|
pub fn randomize_runs(
runs : ArrayView[ExperimentRun],
seed~ : Int64,
) -> Array[ExperimentRun] raise @domain.ExtractionError {
let remaining = runs.to_owned()
let result : Array[ExperimentRun] = []
let mut generator = DeterministicRng::new(seed)
while remaining.length() > 0 {
let (next, index) = generator.integer(lower=0, upper=remaining.length() - 1)
generator = next
let chosen = remaining[index]
ignore(remaining.remove(index))
result.push({
run_number: result.length() + 1,
block: chosen.block,
coded_values: chosen.coded_values,
actual_values: chosen.actual_values,
center_point: chosen.center_point,
response: chosen.response,
})
}
result
}
///|
pub fn set_response(
run : ExperimentRun,
response : Double,
) -> ExperimentRun raise @domain.ExtractionError {
@domain.require_finite("experiment response", response)
{
run_number: run.run_number,
block: run.block,
coded_values: run.coded_values,
actual_values: run.actual_values,
center_point: run.center_point,
response: Some(response),
}
}
///|
pub(all) struct DesignSummary {
runs : Int
factors : Int
center_points : Int
blocks : Int
completed : Int
response_mean : Double
response_range : Double
} derive(Debug, Eq, ToJson)
///|
pub fn summarize_design(
runs : ArrayView[ExperimentRun],
) -> DesignSummary raise @domain.ExtractionError {
if runs.length() == 0 {
raise @domain.ExtractionError::InsufficientData(required=1, actual=0)
}
let mut center = 0
let mut completed = 0
let mut sum = 0.0
let mut minimum = 1.0e300
let mut maximum = -1.0e300
let mut blocks = 0
for run in runs {
if run.center_point {
center += 1
}
blocks = blocks.max(run.block)
match run.response {
Some(value) => {
completed += 1
sum += value
minimum = minimum.min(value)
maximum = maximum.max(value)
}
None => ()
}
}
{
runs: runs.length(),
factors: runs[0].coded_values.length(),
center_points: center,
blocks,
completed,
response_mean: if completed == 0 {
0.0
} else {
sum / completed.to_double()
},
response_range: if completed == 0 {
0.0
} else {
maximum - minimum
},
}
}
///|
pub fn design_matrix(runs : ArrayView[ExperimentRun]) -> Array[Array[Double]] {
[
for run in runs => {
let values = [1.0]
for value in run.coded_values {
values.push(value)
}
for index in 0.. Double raise @domain.ExtractionError {
let values = [
for run in runs if run.center_point => {
match run.response {
Some(value) => value
None => 0.0
}
}
]
if values.length() < 2 {
raise @domain.ExtractionError::InsufficientData(
required=2,
actual=values.length(),
)
}
let mean = values.fold(init=0.0, (sum, value) => sum + value) /
values.length().to_double()
values.fold(init=0.0, (sum, value) => sum + (value - mean) * (value - mean)) /
(values.length() - 1).to_double()
}