diff --git a/examples/simple/agent_objectives.csv b/examples/simple/agent_objectives.csv index 7dd563c86..d9a1b8d9f 100644 --- a/examples/simple/agent_objectives.csv +++ b/examples/simple/agent_objectives.csv @@ -1,5 +1,9 @@ -agent_id,objective_type,decision_weight,decision_lexico_tolerance -A0_GEX,lcox,, -A0_GPR,lcox,, -A0_ELC,lcox,, -A0_RES,eac,, +agent_id,year,objective_type,decision_weight,decision_lexico_tolerance +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 5deec2d0b..9d041d9e7 100644 --- a/src/agent.rs +++ b/src/agent.rs @@ -67,6 +67,8 @@ pub enum DecisionRule { pub struct AgentObjective { /// Unique agent id identifying the agent this objective belongs to pub agent_id: String, + /// The year the objective is relevant for + 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. diff --git a/src/input.rs b/src/input.rs index f7327f467..400bdb197 100644 --- a/src/input.rs +++ b/src/input.rs @@ -214,7 +214,13 @@ pub fn load_model>(model_dir: P) -> Result<(Model, AssetPool)> { &time_slice_info, years, )?; - let agents = read_agents(model_dir.as_ref(), &commodities, &processes, ®ion_ids)?; + let agents = read_agents( + model_dir.as_ref(), + &commodities, + &processes, + ®ion_ids, + years, + )?; let agent_ids = agents.keys().cloned().collect(); let assets = read_assets(model_dir.as_ref(), &agent_ids, &processes, ®ion_ids)?; diff --git a/src/input/agent.rs b/src/input/agent.rs index 540b628bb..695fc380b 100644 --- a/src/input/agent.rs +++ b/src/input/agent.rs @@ -57,13 +57,14 @@ pub fn read_agents( commodities: &CommodityMap, processes: &ProcessMap, region_ids: &HashSet>, + milestone_years: &[u32], ) -> Result { let process_ids = processes.keys().cloned().collect(); let mut agents = read_agents_file(model_dir, commodities, &process_ids)?; let agent_ids = agents.keys().cloned().collect(); let mut agent_regions = read_agent_regions(model_dir, &agent_ids, region_ids)?; - let mut objectives = read_agent_objectives(model_dir, &agents)?; + let mut objectives = read_agent_objectives(model_dir, &agents, milestone_years)?; for (id, agent) in agents.iter_mut() { agent.regions = agent_regions.remove(id).unwrap(); diff --git a/src/input/agent/objective.rs b/src/input/agent/objective.rs index 73b7ea435..94059e7d1 100644 --- a/src/input/agent/objective.rs +++ b/src/input/agent/objective.rs @@ -22,16 +22,18 @@ define_id_getter! {Agent} pub fn read_agent_objectives( model_dir: &Path, agents: &AgentMap, + milestone_years: &[u32], ) -> Result, Vec>> { 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) + read_agent_objectives_from_iter(agent_objectives_csv, agents, milestone_years) .with_context(|| input_err_msg(&file_path)) } fn read_agent_objectives_from_iter( iter: I, agents: &AgentMap, + milestone_years: &[u32], ) -> Result, Vec>> where I: Iterator, @@ -45,6 +47,13 @@ where // Check that required parameters are present and others are absent check_objective_parameter(&objective, &agent.decision_rule)?; + // Check that the year is a valid milestone year + ensure!( + milestone_years.binary_search(&objective.year).is_ok(), + "Invalid milestone year {}", + objective.year + ); + // Append to Vec with the corresponding key or create objectives .entry(Rc::clone(id)) @@ -52,10 +61,20 @@ where .push(objective); } - ensure!( - objectives.len() >= agents.len(), - "All agents must have at least one objective" - ); + // Validate that each agent has at least one objective for each milestone 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 + ); + } + } Ok(objectives) } @@ -119,6 +138,7 @@ mod tests { ($decision_weight:expr, $decision_lexico_tolerance:expr) => { AgentObjective { agent_id: "agent".into(), + year: 2020, objective_type: ObjectiveType::EquivalentAnnualCost, decision_weight: $decision_weight, decision_lexico_tolerance: $decision_lexico_tolerance, @@ -181,10 +201,12 @@ mod tests { )] .into_iter() .collect(); + let milestone_years = [2020]; // Valid let objective = AgentObjective { agent_id: "agent".into(), + year: 2020, objective_type: ObjectiveType::EquivalentAnnualCost, decision_weight: None, decision_lexico_tolerance: None, @@ -192,19 +214,38 @@ mod tests { let expected = [("agent".into(), vec![objective.clone()])] .into_iter() .collect(); - let actual = read_agent_objectives_from_iter([objective].into_iter(), &agents).unwrap(); + let actual = read_agent_objectives_from_iter( + [objective.clone()].into_iter(), + &agents, + &milestone_years, + ) + .unwrap(); assert_eq!(actual, expected); // Missing objective for agent - assert!(read_agent_objectives_from_iter([].into_iter(), &agents).is_err()); + assert!( + read_agent_objectives_from_iter([].into_iter(), &agents, &milestone_years).is_err() + ); + + // Missing objective for milestone year + assert!( + read_agent_objectives_from_iter([objective].into_iter(), &agents, &[2020, 2030]) + .is_err() + ); // Bad parameter - let objective = AgentObjective { + let bad_objective = AgentObjective { agent_id: "agent".into(), + year: 2020, objective_type: ObjectiveType::EquivalentAnnualCost, - decision_weight: Some(1.0), + decision_weight: Some(1.0), // Should only accept None for DecisionRule::Single decision_lexico_tolerance: None, }; - assert!(read_agent_objectives_from_iter([objective].into_iter(), &agents).is_err()); + assert!(read_agent_objectives_from_iter( + [bad_objective].into_iter(), + &agents, + &milestone_years + ) + .is_err()); } }