Skip to content
Open
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
2 changes: 1 addition & 1 deletion Cargo.toml
Original file line number Diff line number Diff line change
Expand Up @@ -31,7 +31,7 @@ indexmap = "2.14.0"
human-panic = "2.0.8"
clap-markdown = "0.1.5"
platform-info = "2.1.0"
derive_more = {version = "2.1", features = ["add", "display"]}
derive_more = {version = "2.1", features = ["add", "display", "from_str"]}
petgraph = "0.8.3"
strum = {version = "0.28.0", features = ["derive"]}
documented = "0.9.2"
Expand Down
19 changes: 18 additions & 1 deletion src/input.rs
Original file line number Diff line number Diff line change
Expand Up @@ -29,6 +29,10 @@ mod region;
use region::read_regions;
mod time_slice;
use time_slice::read_time_slice_info;
mod range;
use range::{parse_range, parse_range_parts, partition};
mod year;
use year::parse_year_str;

/// A trait which provides a method to insert a key and value into a map
pub trait Insert<K, V> {
Expand Down Expand Up @@ -174,7 +178,20 @@ where
T: PartialOrd + Clone,
I: IntoIterator<Item = T>,
{
iter.into_iter().tuple_windows().all(|(a, b)| a < b)
is_sorted_and_unique_with(iter, |a, b| a < b)
}

/// Check whether an iterator contains values that are sorted and unique, comparing with a custom
/// function
pub fn is_sorted_and_unique_with<T, I, F>(iter: I, mut less_than: F) -> bool
where
T: Clone,
I: IntoIterator<Item = T>,
F: FnMut(&T, &T) -> bool,
{
iter.into_iter()
.tuple_windows()
.all(|(a, b)| less_than(&a, &b))
}

/// Insert a key-value pair into a map implementing the `Insert` trait if the key does not
Expand Down
2 changes: 1 addition & 1 deletion src/input/agent/commodity_portion.rs
Original file line number Diff line number Diff line change
Expand Up @@ -3,9 +3,9 @@ use super::super::{deserialise_proportion_nonzero, input_err_msg, read_csv, try_
use crate::agent::{AgentCommodityPortionsMap, AgentID, AgentMap};
use crate::commodity::{CommodityMap, CommodityType};
use crate::id::IDCollection;
use crate::input::parse_year_str;
use crate::region::RegionID;
use crate::units::Dimensionless;
use crate::year::parse_year_str;
use anyhow::{Context, Result, ensure};
use float_cmp::approx_eq;
use indexmap::IndexSet;
Expand Down
2 changes: 1 addition & 1 deletion src/input/agent/objective.rs
Original file line number Diff line number Diff line change
@@ -1,8 +1,8 @@
//! Code for reading agent objectives from a CSV file.
use super::super::{input_err_msg, read_csv, try_insert};
use crate::agent::{AgentID, AgentMap, AgentObjectiveMap, DecisionRule, ObjectiveType};
use crate::input::parse_year_str;
use crate::units::Dimensionless;
use crate::year::parse_year_str;
use anyhow::{Context, Result, ensure};
use itertools::Itertools;
use serde::Deserialize;
Expand Down
2 changes: 1 addition & 1 deletion src/input/agent/search_space.rs
Original file line number Diff line number Diff line change
Expand Up @@ -3,8 +3,8 @@ use super::super::{input_err_msg, read_csv_optional, try_insert};
use crate::agent::{Agent, AgentID, AgentMap, AgentSearchSpaceMap};
use crate::commodity::CommodityID;
use crate::id::IDCollection;
use crate::input::parse_year_str;
use crate::process::{Process, ProcessMap};
use crate::year::parse_year_str;
use anyhow::{Context, Result};
use itertools::Itertools;
use serde::Deserialize;
Expand Down
2 changes: 1 addition & 1 deletion src/input/commodity/levy.rs
Original file line number Diff line number Diff line change
Expand Up @@ -2,10 +2,10 @@
use super::super::{input_err_msg, read_csv_optional, try_insert};
use crate::commodity::{BalanceType, CommodityID, CommodityLevyMap};
use crate::id::IDCollection;
use crate::input::parse_year_str;
use crate::region::{RegionID, parse_region_str};
use crate::time_slice::TimeSliceInfo;
use crate::units::MoneyPerFlow;
use crate::year::parse_year_str;
use anyhow::{Context, Result, ensure};
use indexmap::IndexSet;
use log::warn;
Expand Down
102 changes: 5 additions & 97 deletions src/input/process/availability.rs
Original file line number Diff line number Diff line change
@@ -1,11 +1,11 @@
//! Code for reading process availabilities from a CSV file.
use super::super::{input_err_msg, read_csv_optional, try_insert};
use super::super::{input_err_msg, parse_range, read_csv_optional, try_insert};
use crate::input::parse_year_str;
use crate::process::{ActivityLimits, ProcessActivityLimitsMap, ProcessID, ProcessMap};
use crate::region::parse_region_str;
use crate::time_slice::TimeSliceInfo;
use crate::units::{Dimensionless, Year};
use crate::year::parse_year_str;
use anyhow::{Context, Result, ensure};
use anyhow::{Context, Result};
use itertools::iproduct;
use serde::Deserialize;
use std::collections::HashMap;
Expand Down Expand Up @@ -33,7 +33,8 @@ impl ProcessAvailabilityRaw {
/// capacity.
fn to_bounds(&self, length: Year) -> Result<RangeInclusive<Dimensionless>> {
// Parse availability_range string
let availability_range = parse_availabilities_string(&self.limits)?;
let availability_range = parse_range(&self.limits, Dimensionless(0.0)..=Dimensionless(1.0))
.with_context(|| format!("Could not parse availabilities range: {}", &self.limits))?;

// Convert to bounds based on fraction of the year covered
let ts_frac = length / Year(1.0);
Expand All @@ -43,61 +44,6 @@ impl ProcessAvailabilityRaw {
}
}

/// Parse a string representing availability limits into a range.
fn parse_availabilities_string(s: &str) -> Result<RangeInclusive<Dimensionless>> {
// Disallow empty string
ensure!(!s.trim().is_empty(), "Availability range cannot be empty");

// Require exactly one ".." separator so only forms lower..upper, lower.. or ..upper are allowed.
let parts: Vec<&str> = s.split("..").collect();
ensure!(
parts.len() == 2,
"Availability range must be of the form 'lower..upper', 'lower..' or '..upper'. Invalid: {s}"
);
let left = parts[0].trim();
let right = parts[1].trim();

// Parse lower limit
let lower = if left.is_empty() {
Dimensionless(0.0)
} else {
Dimensionless(
left.parse::<f64>()
.ok()
.with_context(|| format!("Invalid lower availability limit: {left}"))?,
)
};

// Parse upper limit
let upper = if right.is_empty() {
Dimensionless(1.0)
} else {
Dimensionless(
right
.parse::<f64>()
.ok()
.with_context(|| format!("Invalid upper availability limit: {right}"))?,
)
};

// Validation checks
ensure!(
upper >= lower,
"Upper availability limit must be greater than or equal to lower limit. Invalid: {s}"
);
ensure!(
lower >= Dimensionless(0.0),
"Lower availability limit must be >= 0. Invalid: {s}"
);
ensure!(
upper <= Dimensionless(1.0),
"Upper availability limit must be <= 1. Invalid: {s}"
);

// Return range
Ok(lower..=upper)
}

/// Read the process availabilities CSV file.
///
/// This file contains information about the availability of processes over the course of a year as
Expand Down Expand Up @@ -216,7 +162,6 @@ where
#[cfg(test)]
mod tests {
use super::*;
use crate::fixture::assert_error;
use float_cmp::assert_approx_eq;
use rstest::rstest;

Expand All @@ -230,43 +175,6 @@ mod tests {
}
}

#[rstest]
#[case("0.1..0.9", Dimensionless(0.1)..=Dimensionless(0.9))]
#[case("..0.9", Dimensionless(0.0)..=Dimensionless(0.9))] // Empty lower
#[case("0.1..", Dimensionless(0.1)..=Dimensionless(1.0))] // Empty upper
#[case("0.5..0.5", Dimensionless(0.5)..=Dimensionless(0.5))] // Equality
fn parse_availabilities_string_valid(
#[case] input: &str,
#[case] expected: RangeInclusive<Dimensionless>,
) {
assert_eq!(parse_availabilities_string(input).unwrap(), expected);
}

#[rstest]
#[case("", "Availability range cannot be empty")]
#[case(
"0.6..0.5",
"Upper availability limit must be greater than or equal to lower limit. Invalid: 0.6..0.5"
)]
#[case(
"..0.1..0.9",
"Availability range must be of the form 'lower..upper', 'lower..' or '..upper'. Invalid: ..0.1..0.9"
)]
#[case("0.1...0.9", "Invalid upper availability limit: .0.9")]
#[case(
"-0.1..0.5",
"Lower availability limit must be >= 0. Invalid: -0.1..0.5"
)]
#[case("0.1..1.5", "Upper availability limit must be <= 1. Invalid: 0.1..1.5")]
#[case("abc..0.5", "Invalid lower availability limit: abc")]
#[case(
"0.5",
"Availability range must be of the form 'lower..upper', 'lower..' or '..upper'. Invalid: 0.5"
)]
fn parse_availabilities_string_invalid(#[case] input: &str, #[case] error_msg: &str) {
assert_error!(parse_availabilities_string(input), error_msg);
}

#[rstest]
#[case("0.1..", Year(0.1), Dimensionless(0.01)..=Dimensionless(0.1))] // Lower bound
#[case("..0.5", Year(0.1), Dimensionless(0.0)..=Dimensionless(0.05))] // Upper bound
Expand Down
2 changes: 1 addition & 1 deletion src/input/process/flow.rs
Original file line number Diff line number Diff line change
@@ -1,12 +1,12 @@
//! Code for reading process flows from a CSV file.
use super::super::{input_err_msg, read_csv};
use crate::commodity::{CommodityID, CommodityMap, CommodityType};
use crate::input::parse_year_str;
use crate::process::{
FlowDirection, FlowType, ProcessFlow, ProcessFlowsMap, ProcessID, ProcessMap,
};
use crate::region::{RegionID, parse_region_str};
use crate::units::{FlowPerActivity, MoneyPerFlow};
use crate::year::parse_year_str;
use anyhow::{Context, Result, bail, ensure};
use indexmap::{IndexMap, IndexSet};
use itertools::iproduct;
Expand Down
2 changes: 1 addition & 1 deletion src/input/process/investment_constraints.rs
Original file line number Diff line number Diff line change
@@ -1,12 +1,12 @@
//! Code for reading process investment constraints from a CSV file.
use super::super::input_err_msg;
use crate::input::parse_year_str;
use crate::input::{read_csv_optional, try_insert};
use crate::process::{
ProcessID, ProcessInvestmentConstraint, ProcessInvestmentConstraintsMap, ProcessMap,
};
use crate::region::parse_region_str;
use crate::units::{CapacityPerYear, Year};
use crate::year::parse_year_str;
use anyhow::{Context, Result, ensure};
use itertools::iproduct;
use serde::Deserialize;
Expand Down
2 changes: 1 addition & 1 deletion src/input/process/parameter.rs
Original file line number Diff line number Diff line change
@@ -1,9 +1,9 @@
//! Code for reading process parameters from a CSV file
use super::super::{format_items_with_cap, input_err_msg, read_csv, try_insert};
use crate::input::parse_year_str;
use crate::process::{ProcessID, ProcessMap, ProcessParameter, ProcessParameterMap};
use crate::region::parse_region_str;
use crate::units::{Dimensionless, MoneyPerActivity, MoneyPerCapacity, MoneyPerCapacityPerYear};
use crate::year::parse_year_str;
use ::log::warn;
use anyhow::{Context, Result, ensure};
use serde::Deserialize;
Expand Down
Loading
Loading