From 480745ae3917c6fbe3e56efeb3f9951c5ef5784d Mon Sep 17 00:00:00 2001 From: Tom Bland Date: Tue, 18 Mar 2025 16:06:22 +0000 Subject: [PATCH 1/5] Remove parameter from objectives table --- examples/simple/agent_objectives.csv | 18 +++++++++--------- src/agent.rs | 2 -- src/input/agent/objective.rs | 26 +++++++------------------- 3 files changed, 16 insertions(+), 30 deletions(-) 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/src/agent.rs b/src/agent.rs index 9d041d9e7..44782c007 100644 --- a/src/agent.rs +++ b/src/agent.rs @@ -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/objective.rs b/src/input/agent/objective.rs index 94059e7d1..b441324bb 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 => { 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 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(), From b73dd481d90a358062290d3bcfd176979b598e80 Mon Sep 17 00:00:00 2001 From: Tom Bland Date: Tue, 18 Mar 2025 16:23:02 +0000 Subject: [PATCH 2/5] Add decision_lexico_tolerance parameter to agents table --- examples/simple/agents.csv | 10 +++++----- src/agent.rs | 2 ++ src/input/agent.rs | 9 +++++++++ src/input/agent/objective.rs | 1 + 4 files changed, 17 insertions(+), 5 deletions(-) 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 44782c007..777478c04 100644 --- a/src/agent.rs +++ b/src/agent.rs @@ -37,6 +37,8 @@ pub struct Agent { pub regions: RegionSelection, /// The agent's objectives. pub objectives: Vec, + /// The tolerance around the main objective to consider secondary objectives + pub decision_lexico_tolerance: Option, } /// Which processes apply to this agent diff --git a/src/input/agent.rs b/src/input/agent.rs index 695fc380b..5ed398769 100644 --- a/src/input/agent.rs +++ b/src/input/agent.rs @@ -38,6 +38,8 @@ struct AgentRaw { 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. @@ -135,6 +137,7 @@ where annual_cost_limit: agent_raw.annual_cost_limit, regions: RegionSelection::default(), objectives: Vec::new(), + decision_lexico_tolerance: agent_raw.decision_lexico_tolerance, }; ensure!( @@ -179,6 +182,7 @@ mod tests { decision_rule: DecisionRule::Single, capex_limit: None, annual_cost_limit: None, + decision_lexico_tolerance: None, }; let agent_out = Agent { id: "agent".into(), @@ -191,6 +195,7 @@ mod tests { annual_cost_limit: None, regions: RegionSelection::default(), objectives: Vec::new(), + decision_lexico_tolerance: None, }; let expected = AgentMap::from_iter(iter::once(("agent".into(), agent_out))); let actual = @@ -207,6 +212,7 @@ mod tests { decision_rule: DecisionRule::Single, 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()); @@ -220,6 +226,7 @@ mod tests { decision_rule: DecisionRule::Single, 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()); @@ -234,6 +241,7 @@ mod tests { decision_rule: DecisionRule::Single, capex_limit: None, annual_cost_limit: None, + decision_lexico_tolerance: None, }, AgentRaw { id: "agent".into(), @@ -244,6 +252,7 @@ mod tests { decision_rule: DecisionRule::Single, capex_limit: None, annual_cost_limit: None, + decision_lexico_tolerance: None, }, ]; assert!( diff --git a/src/input/agent/objective.rs b/src/input/agent/objective.rs index b441324bb..e836934c7 100644 --- a/src/input/agent/objective.rs +++ b/src/input/agent/objective.rs @@ -187,6 +187,7 @@ mod tests { annual_cost_limit: None, regions: RegionSelection::All, objectives: Vec::new(), + decision_lexico_tolerance: None, }, )] .into_iter() From 871832d8d6bf949e2f537c3d93291fcf39f71d24 Mon Sep 17 00:00:00 2001 From: Tom Bland Date: Tue, 18 Mar 2025 18:35:54 +0000 Subject: [PATCH 3/5] Incorporate tolerance parameter into DecisionRule --- src/agent.rs | 12 ++++----- src/input/agent.rs | 48 +++++++++++++++++++++++++++++------- src/input/agent/objective.rs | 5 ++-- 3 files changed, 46 insertions(+), 19 deletions(-) diff --git a/src/agent.rs b/src/agent.rs index 777478c04..9a7cfd7f8 100644 --- a/src/agent.rs +++ b/src/agent.rs @@ -37,8 +37,6 @@ pub struct Agent { pub regions: RegionSelection, /// The agent's objectives. pub objectives: Vec, - /// The tolerance around the main objective to consider secondary objectives - pub decision_lexico_tolerance: Option, } /// Which processes apply to this agent @@ -51,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 + tolerance: f64, + }, } /// An objective for an agent with associated parameters diff --git a/src/input/agent.rs b/src/input/agent.rs index 5ed398769..83e22451f 100644 --- a/src/input/agent.rs +++ b/src/input/agent.rs @@ -33,7 +33,7 @@ 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. @@ -126,18 +126,35 @@ where } }; + // Parse decision rule + let decision_rule = match agent_raw.decision_rule.as_str() { + "single" => DecisionRule::Single, + "weighted" => DecisionRule::Weighted, + "lexico" => { + let tolerance = agent_raw + .decision_lexico_tolerance + .ok_or_else(|| anyhow::anyhow!("Missing tolerance for lexico decision rule"))?; + ensure!( + tolerance >= 0.0, + "Lexico tolerance must be non-negative, got {}", + tolerance + ); + DecisionRule::Lexicographical { tolerance } + } + invalid_rule => return Err(anyhow::anyhow!("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(), objectives: Vec::new(), - decision_lexico_tolerance: agent_raw.decision_lexico_tolerance, }; ensure!( @@ -179,7 +196,7 @@ 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, @@ -195,7 +212,6 @@ mod tests { annual_cost_limit: None, regions: RegionSelection::default(), objectives: Vec::new(), - decision_lexico_tolerance: None, }; let expected = AgentMap::from_iter(iter::once(("agent".into(), agent_out))); let actual = @@ -209,7 +225,7 @@ 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, @@ -223,7 +239,7 @@ 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, @@ -238,7 +254,7 @@ 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, @@ -249,7 +265,7 @@ 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, @@ -258,5 +274,19 @@ mod tests { 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 e836934c7..27eec8489 100644 --- a/src/input/agent/objective.rs +++ b/src/input/agent/objective.rs @@ -113,7 +113,7 @@ fn check_objective_parameter( DecisionRule::Weighted => { check_field_some!(decision_weight); } - DecisionRule::Lexicographical => { + DecisionRule::Lexicographical { tolerance: _ } => { check_field_none!(decision_weight); } }; @@ -157,7 +157,7 @@ mod tests { assert!(check_objective_parameter(&objective, &decision_rule).is_err()); // DecisionRule::Lexicographical - let decision_rule = DecisionRule::Lexicographical; + let decision_rule = DecisionRule::Lexicographical { tolerance: 1.0 }; let objective = objective!(None); assert!(check_objective_parameter(&objective, &decision_rule).is_ok()); let objective = objective!(Some(1.0)); @@ -187,7 +187,6 @@ mod tests { annual_cost_limit: None, regions: RegionSelection::All, objectives: Vec::new(), - decision_lexico_tolerance: None, }, )] .into_iter() From 1fea5f7d44c3b753cc7c484638976b51a871d3f4 Mon Sep 17 00:00:00 2001 From: Tom Bland Date: Tue, 18 Mar 2025 18:46:48 +0000 Subject: [PATCH 4/5] More detailed parameter info --- src/agent.rs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/agent.rs b/src/agent.rs index 9a7cfd7f8..9289d25f3 100644 --- a/src/agent.rs +++ b/src/agent.rs @@ -57,7 +57,7 @@ pub enum DecisionRule { Weighted, /// Objectives are considered in a specific order Lexicographical { - /// The tolerance around the main objective to consider secondary objectives + /// 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, }, } From 52a8abe266a166f729a9736cbaa651b73ba01f6e Mon Sep 17 00:00:00 2001 From: Tom Bland Date: Wed, 19 Mar 2025 08:39:35 +0000 Subject: [PATCH 5/5] Apply suggestions from code review --- src/input/agent.rs | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/src/input/agent.rs b/src/input/agent.rs index 83e22451f..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; @@ -127,13 +127,13 @@ where }; // Parse decision rule - let decision_rule = match agent_raw.decision_rule.as_str() { + 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 - .ok_or_else(|| anyhow::anyhow!("Missing tolerance for lexico decision rule"))?; + .with_context(|| "Missing tolerance for lexico decision rule")?; ensure!( tolerance >= 0.0, "Lexico tolerance must be non-negative, got {}", @@ -141,7 +141,7 @@ where ); DecisionRule::Lexicographical { tolerance } } - invalid_rule => return Err(anyhow::anyhow!("Invalid decision rule: {}", invalid_rule)), + invalid_rule => bail!("Invalid decision rule: {}", invalid_rule), }; let agent = Agent {