diff --git a/examples/simple/agent_objectives.csv b/examples/simple/agent_objectives.csv index d9a1b8d9f..89ec1fb12 100644 --- a/examples/simple/agent_objectives.csv +++ b/examples/simple/agent_objectives.csv @@ -1,9 +1,9 @@ -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,, +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, diff --git a/examples/simple/agents.csv b/examples/simple/agents.csv index 265475e4b..8114bb9f2 100644 --- a/examples/simple/agents.csv +++ b/examples/simple/agents.csv @@ -1,5 +1,5 @@ -id,description,commodity_id,commodity_portion,search_space,decision_rule,capex_limit,annual_cost_limit -A0_GEX,Gas extractors,GASPRD,1,,single,, -A0_GPR,Gas processors,GASNAT,1,,single,, -A0_ELC,Electricity generators,ELCTRI,1,,single,, -A0_RES,Residential consumer,RSHEAT,1,,single,, +id,description,commodity_id,commodity_portion,search_space,decision_rule,capex_limit,annual_cost_limit,decision_lexico_tolerance +A0_GEX,Gas extractors,GASPRD,1,,single,,, +A0_GPR,Gas processors,GASNAT,1,,single,,, +A0_ELC,Electricity generators,ELCTRI,1,,single,,, +A0_RES,Residential consumer,RSHEAT,1,,single,,, diff --git a/src/agent.rs b/src/agent.rs index 9d041d9e7..9289d25f3 100644 --- a/src/agent.rs +++ b/src/agent.rs @@ -49,17 +49,17 @@ pub enum SearchSpace { } /// The decision rule for a particular objective -#[derive(Debug, Clone, PartialEq, DeserializeLabeledStringEnum)] +#[derive(Debug, Clone, PartialEq)] pub enum DecisionRule { /// Used when there is only a single objective - #[string = "single"] Single, /// A simple weighting of objectives - #[string = "weighted"] Weighted, /// Objectives are considered in a specific order - #[string = "lexico"] - Lexicographical, + Lexicographical { + /// The tolerance around the main objective to consider secondary objectives. This is an absolute value of maximum deviation in the units of the main objective. + tolerance: f64, + }, } /// An objective for an agent with associated parameters @@ -73,8 +73,6 @@ pub struct AgentObjective { pub objective_type: ObjectiveType, /// For the weighted sum objective, the set of weights to apply to each objective. pub decision_weight: Option, - /// The tolerance around the main objective to consider secondary objectives. This is an absolute value of maximum deviation in the units of the main objective. - pub decision_lexico_tolerance: Option, } /// The type of objective for the agent diff --git a/src/input/agent.rs b/src/input/agent.rs index 695fc380b..e2c045035 100644 --- a/src/input/agent.rs +++ b/src/input/agent.rs @@ -4,7 +4,7 @@ use crate::agent::{Agent, AgentMap, DecisionRule, SearchSpace}; use crate::commodity::CommodityMap; use crate::process::ProcessMap; use crate::region::RegionSelection; -use anyhow::{ensure, Context, Result}; +use anyhow::{bail, ensure, Context, Result}; use serde::Deserialize; use std::collections::HashSet; use std::path::Path; @@ -33,11 +33,13 @@ struct AgentRaw { /// by semicolons or `None`, meaning all processes. search_space: Option, /// The decision rule that the agent uses to decide investment. - decision_rule: DecisionRule, + decision_rule: String, /// The maximum capital cost the agent will pay. capex_limit: Option, /// The maximum annual operating cost (fuel plus var_opex etc) that the agent will pay. annual_cost_limit: Option, + /// The tolerance around the main objective to consider secondary objectives. + decision_lexico_tolerance: Option, } /// Read agents info from various CSV files. @@ -124,13 +126,31 @@ where } }; + // Parse decision rule + let decision_rule = match agent_raw.decision_rule.to_ascii_lowercase().as_str() { + "single" => DecisionRule::Single, + "weighted" => DecisionRule::Weighted, + "lexico" => { + let tolerance = agent_raw + .decision_lexico_tolerance + .with_context(|| "Missing tolerance for lexico decision rule")?; + ensure!( + tolerance >= 0.0, + "Lexico tolerance must be non-negative, got {}", + tolerance + ); + DecisionRule::Lexicographical { tolerance } + } + invalid_rule => bail!("Invalid decision rule: {}", invalid_rule), + }; + let agent = Agent { id: Rc::clone(&agent_raw.id), description: agent_raw.description, commodity: Rc::clone(commodity), commodity_portion: agent_raw.commodity_portion, search_space, - decision_rule: agent_raw.decision_rule, + decision_rule, capex_limit: agent_raw.capex_limit, annual_cost_limit: agent_raw.annual_cost_limit, regions: RegionSelection::default(), @@ -176,9 +196,10 @@ mod tests { commodity_id: "commodity1".into(), commodity_portion: 1.0, search_space: Some("A;B".into()), - decision_rule: DecisionRule::Single, + decision_rule: "single".into(), capex_limit: None, annual_cost_limit: None, + decision_lexico_tolerance: None, }; let agent_out = Agent { id: "agent".into(), @@ -204,9 +225,10 @@ mod tests { commodity_id: "made_up_commodity".into(), commodity_portion: 1.0, search_space: None, - decision_rule: DecisionRule::Single, + decision_rule: "single".into(), capex_limit: None, annual_cost_limit: None, + decision_lexico_tolerance: None, }; assert!(read_agents_file_from_iter(iter::once(agent), &commodities, &process_ids).is_err()); @@ -217,9 +239,10 @@ mod tests { commodity_id: "commodity1".into(), commodity_portion: 1.0, search_space: Some("A;D".into()), - decision_rule: DecisionRule::Single, + decision_rule: "single".into(), capex_limit: None, annual_cost_limit: None, + decision_lexico_tolerance: None, }; assert!(read_agents_file_from_iter(iter::once(agent), &commodities, &process_ids).is_err()); @@ -231,9 +254,10 @@ mod tests { commodity_id: "commodity1".into(), commodity_portion: 1.0, search_space: None, - decision_rule: DecisionRule::Single, + decision_rule: "single".into(), capex_limit: None, annual_cost_limit: None, + decision_lexico_tolerance: None, }, AgentRaw { id: "agent".into(), @@ -241,13 +265,28 @@ mod tests { commodity_id: "commodity1".into(), commodity_portion: 1.0, search_space: None, - decision_rule: DecisionRule::Single, + decision_rule: "single".into(), capex_limit: None, annual_cost_limit: None, + decision_lexico_tolerance: None, }, ]; assert!( read_agents_file_from_iter(agents.into_iter(), &commodities, &process_ids).is_err() ); + + // Lexico tolerance missing for lexico decision rule + let agent = AgentRaw { + id: "agent".into(), + description: "".into(), + commodity_id: "commodity1".into(), + commodity_portion: 1.0, + search_space: None, + decision_rule: "lexico".into(), + capex_limit: None, + annual_cost_limit: None, + decision_lexico_tolerance: None, + }; + assert!(read_agents_file_from_iter(iter::once(agent), &commodities, &process_ids).is_err()); } } diff --git a/src/input/agent/objective.rs b/src/input/agent/objective.rs index 94059e7d1..27eec8489 100644 --- a/src/input/agent/objective.rs +++ b/src/input/agent/objective.rs @@ -109,15 +109,12 @@ fn check_objective_parameter( match decision_rule { DecisionRule::Single => { check_field_none!(decision_weight); - check_field_none!(decision_lexico_tolerance); } DecisionRule::Weighted => { - check_field_none!(decision_lexico_tolerance); check_field_some!(decision_weight); } - DecisionRule::Lexicographical => { + DecisionRule::Lexicographical { tolerance: _ } => { check_field_none!(decision_weight); - check_field_some!(decision_lexico_tolerance); } }; @@ -135,42 +132,35 @@ mod tests { #[test] fn test_check_objective_parameter() { macro_rules! objective { - ($decision_weight:expr, $decision_lexico_tolerance:expr) => { + ($decision_weight:expr) => { AgentObjective { agent_id: "agent".into(), year: 2020, objective_type: ObjectiveType::EquivalentAnnualCost, decision_weight: $decision_weight, - decision_lexico_tolerance: $decision_lexico_tolerance, } }; } // DecisionRule::Single let decision_rule = DecisionRule::Single; - let objective = objective!(None, None); + let objective = objective!(None); assert!(check_objective_parameter(&objective, &decision_rule).is_ok()); - let objective = objective!(Some(1.0), None); - assert!(check_objective_parameter(&objective, &decision_rule).is_err()); - let objective = objective!(None, Some(1.0)); + let objective = objective!(Some(1.0)); assert!(check_objective_parameter(&objective, &decision_rule).is_err()); // DecisionRule::Weighted let decision_rule = DecisionRule::Weighted; - let objective = objective!(Some(1.0), None); + let objective = objective!(Some(1.0)); assert!(check_objective_parameter(&objective, &decision_rule).is_ok()); - let objective = objective!(None, None); - assert!(check_objective_parameter(&objective, &decision_rule).is_err()); - let objective = objective!(None, Some(1.0)); + let objective = objective!(None); assert!(check_objective_parameter(&objective, &decision_rule).is_err()); // DecisionRule::Lexicographical - let decision_rule = DecisionRule::Lexicographical; - let objective = objective!(None, Some(1.0)); + let decision_rule = DecisionRule::Lexicographical { tolerance: 1.0 }; + let objective = objective!(None); assert!(check_objective_parameter(&objective, &decision_rule).is_ok()); - let objective = objective!(None, None); - assert!(check_objective_parameter(&objective, &decision_rule).is_err()); - let objective = objective!(Some(1.0), None); + let objective = objective!(Some(1.0)); assert!(check_objective_parameter(&objective, &decision_rule).is_err()); } @@ -209,7 +199,6 @@ mod tests { year: 2020, objective_type: ObjectiveType::EquivalentAnnualCost, decision_weight: None, - decision_lexico_tolerance: None, }; let expected = [("agent".into(), vec![objective.clone()])] .into_iter() @@ -239,7 +228,6 @@ mod tests { year: 2020, objective_type: ObjectiveType::EquivalentAnnualCost, decision_weight: Some(1.0), // Should only accept None for DecisionRule::Single - decision_lexico_tolerance: None, }; assert!(read_agent_objectives_from_iter( [bad_objective].into_iter(),