-
Notifications
You must be signed in to change notification settings - Fork 4
Add commodity constraints input and validation #1487
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
base: main
Are you sure you want to change the base?
Changes from all commits
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,4 @@ | ||
| commodity_id,region_id,balance_type,years,time_slice,limits | ||
| GASPRD,GBR,prod,2020,winter.day,12.34..56.78 | ||
| ELCTRI,GBR,cons,2020,summer,3.14.. | ||
| CO2EMT,GBR,prod,2030,annual,..1.618034 |
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,29 @@ | ||
| $schema: https://specs.frictionlessdata.io/schemas/table-schema.json | ||
| description: Specifies constraints on commodities. | ||
|
|
||
| fields: | ||
| - name: commodity_id | ||
| type: string | ||
| description: The commodity to which this constraint applies | ||
| - name: region_id | ||
| type: string | ||
| description: The region in which this constraint applies | ||
| - name: balance_type | ||
| type: string | ||
| description: The type of balance to which this is applied | ||
| notes: | ||
| The "net" option is not valid here, as net production is already constrained by | ||
| the commodity balance constraints. | ||
| - name: years | ||
| type: string | ||
| description: The year(s) to which this entry applies | ||
| - name: time_slice | ||
| type: string | ||
| description: The time slice(s) to which this entry applies | ||
| - name: limits | ||
| type: string | ||
| description: Lower and upper limits on the value of the commodity | ||
| notes: | ||
| A string in the format `min..max`, where `min` and `max` are decimal numbers | ||
| (e.g. "0.12..78.9"). Either `min` or `max` can be omitted (e.g "0.12.." or | ||
| "..78.9"), which will set the corresponding limit to 0 or infinite, respectively. |
| Original file line number | Diff line number | Diff line change |
|---|---|---|
|
|
@@ -2,10 +2,11 @@ | |
| use crate::id::{define_id_getter, define_id_type}; | ||
| use crate::region::RegionID; | ||
| use crate::time_slice::{TimeSliceID, TimeSliceLevel, TimeSliceSelection}; | ||
| use crate::units::{Flow, MoneyPerFlow}; | ||
| use crate::units::{Flow, Money, MoneyPerFlow}; | ||
| use indexmap::IndexMap; | ||
| use serde::Deserialize; | ||
| use std::collections::HashMap; | ||
| use std::ops::RangeInclusive; | ||
| use std::rc::Rc; | ||
|
|
||
| define_id_type! {CommodityID, "commodity ID"} | ||
|
|
@@ -16,6 +17,9 @@ pub type CommodityMap = IndexMap<CommodityID, Rc<Commodity>>; | |
| /// A map of [`MoneyPerFlow`]s, keyed by region ID, year and time slice ID for a specific levy | ||
| pub type CommodityLevyMap = HashMap<(RegionID, u32, TimeSliceID), MoneyPerFlow>; | ||
|
|
||
| /// A map of [`CommodityConstraint`]s, keyed by region ID and year | ||
| pub type CommodityConstraintsMap = HashMap<(RegionID, u32), Rc<CommodityConstraint>>; | ||
|
|
||
| /// A map of demand values, keyed by region ID, year and time slice selection | ||
| pub type DemandMap = HashMap<(RegionID, u32, TimeSliceSelection), Flow>; | ||
|
|
||
|
|
@@ -115,6 +119,15 @@ pub enum PricingStrategy { | |
| Unpriced, | ||
| } | ||
|
|
||
| /// A constraint imposed on commodity values | ||
| #[derive(PartialEq, Debug, Clone)] | ||
| pub struct CommodityConstraint { | ||
| /// The range of values the commodity is constrained to lie between | ||
| pub limits: RangeInclusive<Money>, | ||
|
Collaborator
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. This also needs to store the
Contributor
Author
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. That makes sense. I wasn't sure what to do with those. |
||
| } | ||
|
|
||
| impl CommodityConstraint {} | ||
|
|
||
| #[cfg(test)] | ||
| mod tests { | ||
| use super::*; | ||
|
|
||
| Original file line number | Diff line number | Diff line change | ||||
|---|---|---|---|---|---|---|
| @@ -0,0 +1,138 @@ | ||||||
| //! Code for reading commodity constraints from a CSV file. | ||||||
| use super::super::{input_err_msg, read_csv_optional}; | ||||||
| use crate::commodity::{BalanceType, CommodityConstraint, CommodityConstraintsMap, CommodityID}; | ||||||
| use crate::id::IDCollection; | ||||||
| use crate::input::{parse_range, parse_year_str, try_insert}; | ||||||
| use crate::region::{RegionID, parse_region_str}; | ||||||
| use crate::time_slice::TimeSliceInfo; | ||||||
| use crate::units::Money; | ||||||
| use anyhow::{Context, Result, ensure}; | ||||||
| use indexmap::IndexSet; | ||||||
| use serde::Deserialize; | ||||||
| use std::collections::HashMap; | ||||||
| use std::path::Path; | ||||||
| use std::rc::Rc; | ||||||
|
|
||||||
| const COMMODITY_CONSTRAINTS_FILE_NAME: &str = "commodity_constraints.csv"; | ||||||
|
|
||||||
| /// Constraints for each commodity | ||||||
| #[derive(PartialEq, Debug, Deserialize)] | ||||||
| struct CommodityConstraintRaw { | ||||||
| /// Unique identifier for the commodity | ||||||
| commodity_id: String, | ||||||
| /// Region id | ||||||
| region_id: String, | ||||||
| /// Type of balance | ||||||
| balance_type: BalanceType, | ||||||
| /// The year(s) to which the constraint applies | ||||||
| years: String, | ||||||
| /// The time slice to which the constraint applies | ||||||
| time_slice: String, | ||||||
| /// Limits on the value of the commodity | ||||||
| limits: String, | ||||||
| } | ||||||
|
|
||||||
| impl CommodityConstraintRaw { | ||||||
| fn validate(&self) -> Result<()> { | ||||||
| // Only permit single regions for initial implementation | ||||||
| ensure!( | ||||||
| self.region_id != "all" && !self.region_id.contains(";"), | ||||||
| "Only single regions are permitted" | ||||||
| ); | ||||||
|
Comment on lines
+38
to
+41
Collaborator
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. This will be validated by |
||||||
|
|
||||||
| // Net production already constrained by commodity balance constraints | ||||||
| ensure!( | ||||||
| self.balance_type != BalanceType::Net, | ||||||
| "Balance type cannot be 'net' for commodity constraints" | ||||||
| ); | ||||||
|
|
||||||
| Ok(()) | ||||||
| } | ||||||
| } | ||||||
|
|
||||||
| /// Read the commodity constraints CSV file. | ||||||
| /// | ||||||
| /// # Arguments | ||||||
| /// | ||||||
| /// * `model_dir` - Folder containing model configuration files | ||||||
| /// * `commodity_ids` - All possible commodity IDs | ||||||
| /// * `region_ids` - All possible region IDs | ||||||
| /// * `time_slice_info` - Information about time slices | ||||||
| /// * `milestone_years` - All milestone years | ||||||
| /// | ||||||
| /// # Returns | ||||||
| /// | ||||||
| /// A `HashMap<CommodityID, CommodityConstraintsMap>` mapping commodity IDs to their | ||||||
| /// commodity-constraints maps, or an error. | ||||||
| pub fn read_commodity_constraints( | ||||||
|
Collaborator
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. We also want to disallow Another slightly tricky thing is that, since |
||||||
| model_dir: &Path, | ||||||
| commodity_ids: &IndexSet<CommodityID>, | ||||||
| region_ids: &IndexSet<RegionID>, | ||||||
| time_slice_info: &TimeSliceInfo, | ||||||
| milestone_years: &[u32], | ||||||
| ) -> Result<HashMap<CommodityID, CommodityConstraintsMap>> { | ||||||
| let file_path = model_dir.join(COMMODITY_CONSTRAINTS_FILE_NAME); | ||||||
| let commodity_constraints_csv = read_csv_optional(&file_path)?; | ||||||
| read_commodity_constraints_from_iter( | ||||||
| commodity_constraints_csv, | ||||||
| commodity_ids, | ||||||
| region_ids, | ||||||
| time_slice_info, | ||||||
| milestone_years, | ||||||
| ) | ||||||
| .with_context(|| input_err_msg(&file_path)) | ||||||
| } | ||||||
|
|
||||||
| /// Process raw commodity-constraint records into a constraints map. | ||||||
| /// | ||||||
| /// # Arguments | ||||||
| /// | ||||||
| /// * `iter` - Iterator over `CommodityConstraintRaw` records | ||||||
| /// * `commodity_ids` - All possible commodity IDs | ||||||
| /// * `region_ids` - All possible region IDs | ||||||
| /// * `time_slice_info` - Information about time slices | ||||||
| /// * `milestone_years` - All milestone years | ||||||
| /// | ||||||
| /// # Returns | ||||||
| /// | ||||||
| /// A `HashMap<CommodityID, CommodityConstraintsMap>` mapping commodity IDs to their | ||||||
| /// commodity-constraints maps, or an error. | ||||||
| fn read_commodity_constraints_from_iter<I>( | ||||||
| iter: I, | ||||||
| commodity_ids: &IndexSet<CommodityID>, | ||||||
| region_ids: &IndexSet<RegionID>, | ||||||
| time_slice_info: &TimeSliceInfo, | ||||||
| milestone_years: &[u32], | ||||||
| ) -> Result<HashMap<CommodityID, CommodityConstraintsMap>> | ||||||
| where | ||||||
| I: Iterator<Item = CommodityConstraintRaw>, | ||||||
| { | ||||||
| let mut map: HashMap<CommodityID, CommodityConstraintsMap> = HashMap::new(); | ||||||
|
|
||||||
| for record in iter { | ||||||
| record.validate()?; | ||||||
|
|
||||||
| // Extract fields from record | ||||||
| let commodity_id = commodity_ids.get_id(&record.commodity_id)?; | ||||||
| // Validation ensures single region_id, so take that at index 0 | ||||||
| let region_id = parse_region_str(&record.region_id, region_ids)?[0].clone(); | ||||||
|
Collaborator
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more.
Suggested change
|
||||||
| let years = parse_year_str(&record.years, milestone_years)?; | ||||||
| let ts_selection = time_slice_info.get_selection(&record.time_slice)?; | ||||||
| let limits = parse_range(&record.limits, Money(0.0)..=Money(f64::INFINITY)) | ||||||
|
Collaborator
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more.
Suggested change
Limits are on amount of commodity consumed/produced (i.e. |
||||||
| .with_context(|| format!("Could not parse constraint range: {}", record.limits))?; | ||||||
|
|
||||||
| // For each record, store that constraint per year | ||||||
| let commodity_map = map.entry(commodity_id.clone()).or_default(); | ||||||
| for year in &years { | ||||||
| let constraint = Rc::new(CommodityConstraint { | ||||||
| limits: limits.clone(), | ||||||
| }); | ||||||
| try_insert( | ||||||
| commodity_map, | ||||||
| &(region_id.clone(), *year), | ||||||
| constraint.clone(), | ||||||
| )?; | ||||||
| } | ||||||
| } | ||||||
| Ok(map) | ||||||
| } | ||||||
Uh oh!
There was an error while loading. Please reload this page.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
You can have multiple constraints for each (region, year), so this should be
HashMap<(RegionID, u32), Vec<CommodityConstraint>>Not sure you necessarily need the
Rc, but may be wrong (probably). If required, it should probably beArcto keep things parallel-compatibleThere was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Ah OK yeah, I understand what you were referring to in your comment on the issue now.