diff --git a/examples/simple/agent_objectives.csv b/examples/simple/agent_objectives.csv index 89ec1fb12..d277da0d9 100644 --- a/examples/simple/agent_objectives.csv +++ b/examples/simple/agent_objectives.csv @@ -1,9 +1,9 @@ -agent_id,year,objective_type,decision_weight -A0_GEX,2020,lcox, -A0_GEX,2030,lcox, -A0_GPR,2020,lcox, -A0_GPR,2030,lcox, -A0_ELC,2020,lcox, -A0_ELC,2030,lcox, -A0_RES,2020,eac, -A0_RES,2030,eac, +agent_id,year,objective_type,decision_weight,decision_lexico_order +A0_GEX,2020,lcox,, +A0_GEX,2030,lcox,, +A0_GPR,2020,lcox,, +A0_GPR,2030,lcox,, +A0_ELC,2020,lcox,, +A0_ELC,2030,lcox,, +A0_RES,2020,eac,, +A0_RES,2030,eac,, diff --git a/src/agent.rs b/src/agent.rs index 9289d25f3..acf597ad7 100644 --- a/src/agent.rs +++ b/src/agent.rs @@ -71,8 +71,10 @@ pub struct AgentObjective { pub year: u32, /// Acronym identifying the objective (e.g. LCOX) pub objective_type: ObjectiveType, - /// For the weighted sum objective, the set of weights to apply to each objective. + /// For the weighted sum decision rule, the set of weights to apply to each objective. pub decision_weight: Option, + /// For the lexico decision rule, the order in which to consider objectives. + pub decision_lexico_order: Option, } /// The type of objective for the agent diff --git a/src/input/agent/objective.rs b/src/input/agent/objective.rs index 27eec8489..daaf12568 100644 --- a/src/input/agent/objective.rs +++ b/src/input/agent/objective.rs @@ -61,18 +61,17 @@ where .push(objective); } - // Validate that each agent has at least one objective for each milestone year - for (agent_id, _agent) in agents { + // Check that agents have appropriate objectives for their decision rule every year + for (agent_id, agent) in agents { let agent_objectives = objectives .get(agent_id) .with_context(|| format!("Agent {} has no objectives", agent_id))?; for &year in milestone_years { - ensure!( - agent_objectives.iter().any(|obj| obj.year == year), - "Agent {} is missing objectives for milestone year {}", - agent_id, - year - ); + let objectives_for_year: Vec<_> = agent_objectives + .iter() + .filter(|obj| obj.year == year) + .collect(); + check_agent_objectives(&objectives_for_year, &agent.decision_rule, agent_id, year)?; } } @@ -109,18 +108,67 @@ fn check_objective_parameter( match decision_rule { DecisionRule::Single => { check_field_none!(decision_weight); + check_field_none!(decision_lexico_order); } DecisionRule::Weighted => { check_field_some!(decision_weight); + check_field_none!(decision_lexico_order); } DecisionRule::Lexicographical { tolerance: _ } => { check_field_none!(decision_weight); + check_field_some!(decision_lexico_order); } }; Ok(()) } +/// Check that a set of objectives meets the requirements of a decision rule +fn check_agent_objectives( + objectives: &[&AgentObjective], + decision_rule: &DecisionRule, + agent_id: &str, + year: u32, +) -> Result<()> { + let count = objectives.len(); + match decision_rule { + DecisionRule::Single => { + ensure!( + count == 1, + "Agent {} has {} objectives for milestone year {} but should have exactly 1", + agent_id, + count, + year + ); + } + DecisionRule::Weighted => { + ensure!( + count > 1, + "Agent {} has {} objectives for milestone year {} but should have more than 1", + agent_id, + count, + year + ); + } + DecisionRule::Lexicographical { tolerance: _ } => { + let mut lexico_orders: Vec = objectives + .iter() + .filter_map(|obj| obj.decision_lexico_order) + .collect(); + lexico_orders.sort_unstable(); + ensure!( + lexico_orders == [1, 2], + "Agent {} must have objectives with decision_lexico_order values of 1 and 2 for milestone year {}, but found {:?}", + agent_id, + year, + lexico_orders + ); + } + } + + Ok(()) +} + #[cfg(test)] mod tests { use super::*; @@ -132,35 +180,42 @@ mod tests { #[test] fn test_check_objective_parameter() { macro_rules! objective { - ($decision_weight:expr) => { + ($decision_weight:expr, $decision_lexico_order:expr) => { AgentObjective { agent_id: "agent".into(), year: 2020, objective_type: ObjectiveType::EquivalentAnnualCost, decision_weight: $decision_weight, + decision_lexico_order: $decision_lexico_order, } }; } // DecisionRule::Single let decision_rule = DecisionRule::Single; - let objective = objective!(None); + let objective = objective!(None, None); assert!(check_objective_parameter(&objective, &decision_rule).is_ok()); - let objective = objective!(Some(1.0)); + let objective = objective!(Some(1.0), None); + assert!(check_objective_parameter(&objective, &decision_rule).is_err()); + let objective = objective!(None, Some(1)); assert!(check_objective_parameter(&objective, &decision_rule).is_err()); // DecisionRule::Weighted let decision_rule = DecisionRule::Weighted; - let objective = objective!(Some(1.0)); + let objective = objective!(Some(1.0), None); assert!(check_objective_parameter(&objective, &decision_rule).is_ok()); - let objective = objective!(None); + let objective = objective!(None, None); + assert!(check_objective_parameter(&objective, &decision_rule).is_err()); + let objective = objective!(None, Some(1)); assert!(check_objective_parameter(&objective, &decision_rule).is_err()); // DecisionRule::Lexicographical let decision_rule = DecisionRule::Lexicographical { tolerance: 1.0 }; - let objective = objective!(None); + let objective = objective!(None, Some(1)); assert!(check_objective_parameter(&objective, &decision_rule).is_ok()); - let objective = objective!(Some(1.0)); + let objective = objective!(None, None); + assert!(check_objective_parameter(&objective, &decision_rule).is_err()); + let objective = objective!(Some(1.0), None); assert!(check_objective_parameter(&objective, &decision_rule).is_err()); } @@ -199,6 +254,7 @@ mod tests { year: 2020, objective_type: ObjectiveType::EquivalentAnnualCost, decision_weight: None, + decision_lexico_order: None, }; let expected = [("agent".into(), vec![objective.clone()])] .into_iter() @@ -228,6 +284,7 @@ mod tests { year: 2020, objective_type: ObjectiveType::EquivalentAnnualCost, decision_weight: Some(1.0), // Should only accept None for DecisionRule::Single + decision_lexico_order: None, }; assert!(read_agent_objectives_from_iter( [bad_objective].into_iter(), @@ -236,4 +293,43 @@ mod tests { ) .is_err()); } + + #[test] + fn test_check_agent_objectives() { + let objective1 = AgentObjective { + agent_id: "agent".into(), + year: 2020, + objective_type: ObjectiveType::EquivalentAnnualCost, + decision_weight: None, + decision_lexico_order: Some(1), + }; + let objective2 = AgentObjective { + agent_id: "agent".into(), + year: 2020, + objective_type: ObjectiveType::EquivalentAnnualCost, + decision_weight: None, + decision_lexico_order: Some(2), + }; + + // DecisionRule::Single + let decision_rule = DecisionRule::Single; + let objectives = [&objective1]; + assert!(check_agent_objectives(&objectives, &decision_rule, "agent", 2020).is_ok()); + let objectives = [&objective1, &objective2]; + assert!(check_agent_objectives(&objectives, &decision_rule, "agent", 2020).is_err()); + + // DecisionRule::Weighted + let decision_rule = DecisionRule::Weighted; + let objectives = [&objective1, &objective2]; + assert!(check_agent_objectives(&objectives, &decision_rule, "agent", 2020).is_ok()); + let objectives = [&objective1]; + assert!(check_agent_objectives(&objectives, &decision_rule, "agent", 2020).is_err()); + + // DecisionRule::Lexicographical + let decision_rule = DecisionRule::Lexicographical { tolerance: 1.0 }; + let objectives = [&objective1, &objective2]; + assert!(check_agent_objectives(&objectives, &decision_rule, "agent", 2020).is_ok()); + let objectives = [&objective1, &objective1]; + assert!(check_agent_objectives(&objectives, &decision_rule, "agent", 2020).is_err()); + } }