///|
/// Operational effort estimate for a measurement plan.
///
/// The estimate is deliberately expressed in abstract seconds. It is a
/// repeatable planning signal, not a promise about a particular machine.
pub(all) struct DurationEstimate {
step_count : Int
sample_count : Int
seconds : Double
setup_seconds : Double
measurement_seconds : Double
transfer_seconds : Double
}
///|
fn duration_sample_count(strategy : Strategy) -> Int {
match strategy {
SinglePoint => 1
RepeatedPoint(count~) => if count > 0 { count } else { 0 }
Scan(count~) => if count > 0 { count } else { 0 }
}
}
///|
/// Return the measurement time assigned to one strategy.
pub fn strategy_duration_seconds(strategy : Strategy) -> Double {
match strategy {
SinglePoint => 1.0
RepeatedPoint(count~) => 0.8 + count.to_double() * 1.4
Scan(count~) => 1.5 + count.to_double() * 1.9
}
}
///|
/// Return the setup cost of a step, including datum alignment.
pub fn step_setup_seconds(step : MeasurementStep) -> Double {
if step.direction == "datum-aligned" {
2.5
} else {
1.0
}
}
///|
/// Return the transfer cost between two device IDs.
pub fn device_transfer_seconds(previous : String, next : String) -> Double {
if previous == next {
0.0
} else {
3.0
}
}
///|
fn plan_transfer_seconds(steps : Array[MeasurementStep]) -> Double {
if steps.length() < 2 {
0.0
} else {
let mut result = 0.0
for index in 1.. DurationEstimate {
let mut sample_count = 0
let mut setup_seconds = 0.0
let mut measurement_seconds = 0.0
for step in plan.steps {
sample_count += duration_sample_count(step.strategy)
setup_seconds += step_setup_seconds(step)
measurement_seconds += strategy_duration_seconds(step.strategy)
}
let transfer_seconds = plan_transfer_seconds(plan.steps)
{
step_count: plan.steps.length(),
sample_count,
seconds: setup_seconds + measurement_seconds + transfer_seconds,
setup_seconds,
measurement_seconds,
transfer_seconds,
}
}
///|
/// Return a stable operational band for dashboards and reports.
pub fn duration_band(estimate : DurationEstimate) -> String {
if estimate.seconds <= 0.0 {
"empty"
} else if estimate.seconds < 15.0 {
"short"
} else if estimate.seconds < 60.0 {
"medium"
} else {
"long"
}
}
///|
/// Return the estimated seconds per collected sample.
pub fn seconds_per_sample(estimate : DurationEstimate) -> Double {
if estimate.sample_count <= 0 {
0.0
} else {
estimate.seconds / estimate.sample_count.to_double()
}
}
///|
/// Return the fraction of effort spent collecting measurements.
pub fn measurement_time_ratio(estimate : DurationEstimate) -> Double {
if estimate.seconds <= 0.0 {
0.0
} else {
estimate.measurement_seconds / estimate.seconds
}
}
///|
/// Return the fraction of effort spent moving between devices.
pub fn transfer_time_ratio(estimate : DurationEstimate) -> Double {
if estimate.seconds <= 0.0 {
0.0
} else {
estimate.transfer_seconds / estimate.seconds
}
}
///|
/// Return a utilization ratio against a positive available time window.
pub fn duration_utilization(
estimate : DurationEstimate,
available_seconds : Double,
) -> Double {
if available_seconds <= 0.0 {
0.0
} else {
estimate.seconds / available_seconds
}
}
///|
/// Return whether the plan fits inside a positive time window.
pub fn duration_fits(
estimate : DurationEstimate,
available_seconds : Double,
) -> Bool {
available_seconds >= estimate.seconds && available_seconds > 0.0
}
///|
/// Return the number of whole windows needed for this estimate.
pub fn required_time_windows(
estimate : DurationEstimate,
window_seconds : Double,
) -> Int {
if estimate.seconds <= 0.0 || window_seconds <= 0.0 {
0
} else {
let mut windows = 1
let mut capacity = window_seconds
while capacity < estimate.seconds {
windows += 1
capacity += window_seconds
}
windows
}
}
///|
/// Return a compact deterministic duration label for CLI output.
pub fn render_duration(estimate : DurationEstimate) -> String {
duration_band(estimate) +
" steps=" +
estimate.step_count.to_string() +
" samples=" +
estimate.sample_count.to_string() +
" seconds=" +
estimate.seconds.to_string()
}
///|
/// Return a conservative score where lower effort is better.
pub fn duration_efficiency(
estimate : DurationEstimate,
available_seconds : Double,
) -> Double {
let utilization = duration_utilization(estimate, available_seconds)
if utilization <= 0.0 {
0.0
} else if utilization >= 1.0 {
1.0 / utilization
} else {
1.0 - utilization
}
}
///|
/// Return the estimated cost of a single step, including its setup.
pub fn step_duration_seconds(step : MeasurementStep) -> Double {
step_setup_seconds(step) + strategy_duration_seconds(step.strategy)
}
///|
/// Return the maximum individual step cost in a plan.
pub fn maximum_step_duration(plan : MeasurementPlan) -> Double {
let mut result = 0.0
for step in plan.steps {
let seconds = step_duration_seconds(step)
if seconds > result {
result = seconds
}
}
result
}
///|
/// Return the average step cost, or zero for an empty plan.
pub fn average_step_duration(plan : MeasurementPlan) -> Double {
if plan.steps.length() == 0 {
0.0
} else {
estimate_plan_duration(plan).seconds / plan.steps.length().to_double()
}
}
///|
/// Return whether transfers dominate the operational estimate.
pub fn transfer_dominates(estimate : DurationEstimate) -> Bool {
estimate.transfer_seconds > estimate.measurement_seconds
}
///|
/// Return whether the estimate has enough samples for a stable summary.
pub fn duration_has_replicates(estimate : DurationEstimate) -> Bool {
estimate.sample_count > estimate.step_count
}