use super::super::{input_err_msg, read_csv, try_insert};
use crate::agent::{AgentID, AgentMap, AgentObjectiveMap, DecisionRule, ObjectiveType};
use crate::units::Dimensionless;
use crate::year::parse_year_str;
use anyhow::{Context, Result, ensure};
use itertools::Itertools;
use serde::Deserialize;
use std::collections::HashMap;
use std::path::Path;
const AGENT_OBJECTIVES_FILE_NAME: &str = "agent_objectives.csv";
#[derive(Debug, Clone, Deserialize, PartialEq)]
struct AgentObjectiveRaw {
agent_id: AgentID,
years: String,
objective_type: ObjectiveType,
decision_weight: Option<Dimensionless>,
decision_lexico_order: Option<u32>,
}
pub fn read_agent_objectives(
model_dir: &Path,
agents: &AgentMap,
milestone_years: &[u32],
) -> Result<HashMap<AgentID, AgentObjectiveMap>> {
let file_path = model_dir.join(AGENT_OBJECTIVES_FILE_NAME);
let agent_objectives_csv = read_csv(&file_path)?;
read_agent_objectives_from_iter(agent_objectives_csv, agents, milestone_years)
.with_context(|| input_err_msg(&file_path))
}
fn read_agent_objectives_from_iter<I>(
iter: I,
agents: &AgentMap,
milestone_years: &[u32],
) -> Result<HashMap<AgentID, AgentObjectiveMap>>
where
I: Iterator<Item = AgentObjectiveRaw>,
{
let mut all_objectives = HashMap::new();
for objective in iter {
let (id, agent) = agents
.get_key_value(&objective.agent_id)
.context("Invalid agent ID")?;
check_objective_parameter(&objective, &agent.decision_rule)?;
let agent_objectives = all_objectives
.entry(id.clone())
.or_insert_with(AgentObjectiveMap::new);
for year in parse_year_str(&objective.years, milestone_years)? {
try_insert(agent_objectives, &year, objective.objective_type).with_context(|| {
format!("Duplicate agent objective entry for agent {id} and year {year}")
})?;
}
}
for agent_id in agents.keys() {
let agent_objectives = all_objectives
.get(agent_id)
.with_context(|| format!("Agent {agent_id} has no objectives"))?;
let missing_years = milestone_years
.iter()
.copied()
.filter(|year| !agent_objectives.contains_key(year))
.collect_vec();
ensure!(
missing_years.is_empty(),
"Agent {agent_id} is missing objectives for the following milestone years: {missing_years:?}"
);
}
Ok(all_objectives)
}
fn check_objective_parameter(
objective: &AgentObjectiveRaw,
decision_rule: &DecisionRule,
) -> Result<()> {
macro_rules! check_field_none {
($field:ident) => {
ensure!(
objective.$field.is_none(),
"Field {} should be empty for this decision rule",
stringify!($field)
)
};
}
macro_rules! check_field_some {
($field:ident) => {
ensure!(
objective.$field.is_some(),
"Required field {} is empty",
stringify!($field)
)
};
}
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(())
}
#[cfg(test)]
mod tests {
use super::*;
use crate::agent::ObjectiveType;
use crate::fixture::{agents, assert_error};
use rstest::{fixture, rstest};
use std::iter;
macro_rules! objective {
($decision_weight:expr, $decision_lexico_order:expr) => {
AgentObjectiveRaw {
agent_id: "agent".into(),
years: "2020".into(),
objective_type: ObjectiveType::LevelisedCostOfX,
decision_weight: $decision_weight,
decision_lexico_order: $decision_lexico_order,
}
};
}
#[test]
fn check_objective_parameter_single() {
let decision_rule = DecisionRule::Single;
let objective = objective!(None, None);
check_objective_parameter(&objective, &decision_rule).unwrap();
let objective = objective!(Some(Dimensionless(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());
}
#[test]
fn check_objective_parameter_weighted() {
let decision_rule = DecisionRule::Weighted;
let objective = objective!(Some(Dimensionless(1.0)), None);
check_objective_parameter(&objective, &decision_rule).unwrap();
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());
}
#[test]
fn check_objective_parameter_lexico() {
let decision_rule = DecisionRule::Lexicographical { tolerance: 1.0 };
let objective = objective!(None, Some(1));
check_objective_parameter(&objective, &decision_rule).unwrap();
let objective = objective!(None, None);
assert!(check_objective_parameter(&objective, &decision_rule).is_err());
let objective = objective!(Some(Dimensionless(1.0)), None);
assert!(check_objective_parameter(&objective, &decision_rule).is_err());
}
#[fixture]
fn objective_raw() -> AgentObjectiveRaw {
AgentObjectiveRaw {
agent_id: "agent1".into(),
years: "2020".into(),
objective_type: ObjectiveType::LevelisedCostOfX,
decision_weight: None,
decision_lexico_order: None,
}
}
#[rstest]
fn read_agent_objectives_from_iter_valid(agents: AgentMap, objective_raw: AgentObjectiveRaw) {
let milestone_years = [2020];
let expected = iter::once((
"agent1".into(),
iter::once((2020, objective_raw.objective_type)).collect(),
))
.collect();
let actual = read_agent_objectives_from_iter(
iter::once(objective_raw.clone()),
&agents,
&milestone_years,
)
.unwrap();
assert_eq!(actual, expected);
}
#[rstest]
fn read_agent_objectives_from_iter_invalid_no_objective_for_agent(agents: AgentMap) {
assert_error!(
read_agent_objectives_from_iter(iter::empty(), &agents, &[2020]),
"Agent agent1 has no objectives"
);
}
#[rstest]
fn read_agent_objectives_from_iter_invalid_no_objective_for_year(
agents: AgentMap,
objective_raw: AgentObjectiveRaw,
) {
assert_error!(
read_agent_objectives_from_iter(iter::once(objective_raw), &agents, &[2020, 2030]),
"Agent agent1 is missing objectives for the following milestone years: [2030]"
);
}
#[rstest]
fn read_agent_objectives_from_iter_invalid_bad_param(agents: AgentMap) {
let bad_objective = AgentObjectiveRaw {
agent_id: "agent1".into(),
years: "2020".into(),
objective_type: ObjectiveType::LevelisedCostOfX,
decision_weight: Some(Dimensionless(1.0)), decision_lexico_order: None,
};
assert_error!(
read_agent_objectives_from_iter([bad_objective].into_iter(), &agents, &[2020]),
"Field decision_weight should be empty for this decision rule"
);
}
}