use super::compute::{compute_area_illuminance, AreaResult};
use super::layout::{generate_placements, generate_pole_positions, ArrangementType, PoleConfig};
use crate::Eulumdat;
#[derive(Debug, Clone)]
#[cfg_attr(feature = "serde", derive(serde::Serialize, serde::Deserialize))]
pub struct OptimizationCriteria {
pub target_min_lux: f64,
pub target_uniformity: Option<f64>,
pub height_range: (f64, f64),
pub height_step: f64,
pub spacing_range: (f64, f64),
pub arrangement: ArrangementType,
}
impl Default for OptimizationCriteria {
fn default() -> Self {
Self {
target_min_lux: 20.0,
target_uniformity: None,
height_range: (8.0, 14.0),
height_step: 2.0,
spacing_range: (10.0, 60.0),
arrangement: ArrangementType::Single,
}
}
}
#[derive(Debug, Clone, PartialEq)]
#[cfg_attr(feature = "serde", derive(serde::Serialize, serde::Deserialize))]
pub struct OptimizationRow {
pub mounting_height: f64,
pub optimal_spacing: f64,
pub min_lux: f64,
pub avg_lux: f64,
pub max_lux: f64,
pub uniformity_min_avg: f64,
pub uniformity_min_max: f64,
pub meets_criteria: bool,
pub poles_needed: usize,
}
pub fn optimize_spacing(
ldt: &Eulumdat,
criteria: &OptimizationCriteria,
pole_config: &PoleConfig,
area_width: f64,
area_depth: f64,
grid_resolution: usize,
proration_factor: f64,
) -> Vec<OptimizationRow> {
let mut results = Vec::new();
let mut h = criteria.height_range.0;
while h <= criteria.height_range.1 + 0.01 {
let row = optimize_for_height(
ldt,
criteria,
pole_config,
h,
area_width,
area_depth,
grid_resolution,
proration_factor,
);
results.push(row);
h += criteria.height_step;
}
results
}
#[allow(clippy::too_many_arguments)]
fn optimize_for_height(
ldt: &Eulumdat,
criteria: &OptimizationCriteria,
pole_config: &PoleConfig,
height: f64,
area_width: f64,
area_depth: f64,
grid_resolution: usize,
proration_factor: f64,
) -> OptimizationRow {
let (s_min, s_max) = criteria.spacing_range;
let gr = (5.0_f64.sqrt() + 1.0) / 2.0; let tolerance = 0.5;
let mut a = s_min;
let mut b = s_max;
let tight_result = evaluate_spacing(
ldt,
pole_config,
height,
a,
area_width,
area_depth,
grid_resolution,
proration_factor,
);
let tight_meets = meets_criteria(&tight_result, criteria);
if !tight_meets {
return make_row(height, a, &tight_result, false, area_width, area_depth);
}
let wide_result = evaluate_spacing(
ldt,
pole_config,
height,
b,
area_width,
area_depth,
grid_resolution,
proration_factor,
);
let wide_meets = meets_criteria(&wide_result, criteria);
if wide_meets {
return make_row(height, b, &wide_result, true, area_width, area_depth);
}
while (b - a) > tolerance {
let c = b - (b - a) / gr;
let d = a + (b - a) / gr;
let result_c = evaluate_spacing(
ldt,
pole_config,
height,
c,
area_width,
area_depth,
grid_resolution,
proration_factor,
);
let meets_c = meets_criteria(&result_c, criteria);
let result_d = evaluate_spacing(
ldt,
pole_config,
height,
d,
area_width,
area_depth,
grid_resolution,
proration_factor,
);
let meets_d = meets_criteria(&result_d, criteria);
if meets_d {
a = c;
} else if meets_c {
a = c;
b = d;
} else {
b = c;
}
}
let final_result = evaluate_spacing(
ldt,
pole_config,
height,
a,
area_width,
area_depth,
grid_resolution,
proration_factor,
);
let final_meets = meets_criteria(&final_result, criteria);
make_row(
height,
a,
&final_result,
final_meets,
area_width,
area_depth,
)
}
#[allow(clippy::too_many_arguments)]
fn evaluate_spacing(
ldt: &Eulumdat,
pole_config: &PoleConfig,
height: f64,
spacing: f64,
_area_width: f64,
_area_depth: f64,
grid_resolution: usize,
proration_factor: f64,
) -> AreaResult {
let bay = spacing;
let poles = generate_pole_positions(3, 3, bay * 3.0, bay * 3.0);
let placements = generate_placements(&poles, height, pole_config, 0.0);
let mut result = compute_area_illuminance(
ldt,
&placements,
bay * 3.0,
bay * 3.0,
grid_resolution * 3,
proration_factor,
);
let n = grid_resolution;
let mut min_lux = f64::MAX;
let mut max_lux: f64 = 0.0;
let mut sum_lux: f64 = 0.0;
let count = (n * n) as f64;
for row in n..(2 * n) {
for col in n..(2 * n) {
let lux = result.lux_grid[row][col];
if lux < min_lux {
min_lux = lux;
}
if lux > max_lux {
max_lux = lux;
}
sum_lux += lux;
}
}
if min_lux == f64::MAX {
min_lux = 0.0;
}
let avg_lux = if count > 0.0 { sum_lux / count } else { 0.0 };
result.min_lux = min_lux;
result.max_lux = max_lux;
result.avg_lux = avg_lux;
result.uniformity_min_avg = if avg_lux > 0.0 {
min_lux / avg_lux
} else {
0.0
};
result.uniformity_min_max = if max_lux > 0.0 {
min_lux / max_lux
} else {
0.0
};
result
}
fn meets_criteria(result: &AreaResult, criteria: &OptimizationCriteria) -> bool {
if result.min_lux < criteria.target_min_lux {
return false;
}
if let Some(target_u) = criteria.target_uniformity {
if result.uniformity_min_avg < target_u {
return false;
}
}
true
}
fn make_row(
height: f64,
spacing: f64,
result: &AreaResult,
meets: bool,
area_width: f64,
area_depth: f64,
) -> OptimizationRow {
let cols = (area_width / spacing).ceil() as usize;
let rows = (area_depth / spacing).ceil() as usize;
OptimizationRow {
mounting_height: height,
optimal_spacing: spacing,
min_lux: result.min_lux,
avg_lux: result.avg_lux,
max_lux: result.max_lux,
uniformity_min_avg: result.uniformity_min_avg,
uniformity_min_max: result.uniformity_min_max,
meets_criteria: meets,
poles_needed: rows * cols,
}
}
#[cfg(test)]
mod tests {
use super::*;
use crate::LampSet;
fn test_ldt() -> Eulumdat {
Eulumdat {
c_angles: vec![0.0, 90.0, 180.0, 270.0],
g_angles: vec![0.0, 15.0, 30.0, 45.0, 60.0, 75.0, 90.0],
intensities: vec![
vec![300.0, 280.0, 220.0, 140.0, 60.0, 15.0, 3.0],
vec![300.0, 270.0, 200.0, 120.0, 50.0, 12.0, 2.0],
vec![300.0, 280.0, 220.0, 140.0, 60.0, 15.0, 3.0],
vec![300.0, 270.0, 200.0, 120.0, 50.0, 12.0, 2.0],
],
lamp_sets: vec![LampSet {
num_lamps: 1,
total_luminous_flux: 10000.0,
..Default::default()
}],
..Default::default()
}
}
#[test]
fn optimizer_produces_results() {
let ldt = test_ldt();
let criteria = OptimizationCriteria {
target_min_lux: 5.0,
target_uniformity: None,
height_range: (8.0, 12.0),
height_step: 2.0,
spacing_range: (10.0, 40.0),
arrangement: ArrangementType::Single,
};
let pole_cfg = PoleConfig::default();
let results = optimize_spacing(&ldt, &criteria, &pole_cfg, 60.0, 40.0, 20, 1.0);
assert_eq!(results.len(), 3); for row in &results {
assert!(row.optimal_spacing >= 10.0);
assert!(row.poles_needed > 0);
}
}
#[test]
fn higher_poles_allow_wider_spacing() {
let ldt = test_ldt();
let criteria = OptimizationCriteria {
target_min_lux: 5.0,
target_uniformity: None,
height_range: (8.0, 14.0),
height_step: 2.0,
spacing_range: (10.0, 50.0),
arrangement: ArrangementType::Single,
};
let pole_cfg = PoleConfig::default();
let results = optimize_spacing(&ldt, &criteria, &pole_cfg, 60.0, 40.0, 20, 1.0);
assert!(results.len() >= 3);
}
}