Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
18 changes: 9 additions & 9 deletions examples/simple/agent_objectives.csv
Original file line number Diff line number Diff line change
@@ -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,,
4 changes: 3 additions & 1 deletion src/agent.rs
Original file line number Diff line number Diff line change
Expand Up @@ -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<f64>,
/// For the lexico decision rule, the order in which to consider objectives.
pub decision_lexico_order: Option<u32>,
}

/// The type of objective for the agent
Expand Down
126 changes: 111 additions & 15 deletions src/input/agent/objective.rs
Original file line number Diff line number Diff line change
Expand Up @@ -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)?;
}
}

Expand Down Expand Up @@ -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<u32> = 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::*;
Expand All @@ -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());
}

Expand Down Expand Up @@ -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()
Expand Down Expand Up @@ -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(),
Expand All @@ -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());
}
}