Skip to content
Merged
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
3 changes: 0 additions & 3 deletions docs/dispatch_optimisation.md
Original file line number Diff line number Diff line change
Expand Up @@ -52,9 +52,6 @@ for all commodity flows that the process has (except *pac1*). Where *pac1* is th
primary activity commodity for the asset (i.e. all input and output flows are made proportional to
*pac1* flow).

Note – need to handle cases where a flow is set to zero in the input data – should raise a
warning that the value has been ignored, specifying which region/asset/commodity.

**TBD** - cases where time slice level of the commodity is seasonal or annual.

### Commodity-flexible assets
Expand Down
216 changes: 194 additions & 22 deletions src/input/process.rs
Original file line number Diff line number Diff line change
@@ -1,7 +1,8 @@
//! Code for reading process-related information from CSV files.
use crate::commodity::Commodity;
use crate::input::*;
use crate::process::Process;
use crate::process::{Process, ProcessAvailability, ProcessFlow, ProcessParameter};
use crate::region::RegionSelection;
use crate::time_slice::TimeSliceInfo;
use anyhow::Result;
use serde::Deserialize;
Expand Down Expand Up @@ -40,6 +41,9 @@ struct ProcessDescription {
}
define_id_getter! {ProcessDescription}

/// A map of process-related data structures, grouped by process ID
type GroupedMap<T> = HashMap<Rc<str>, Vec<T>>;

/// Read process information from the specified CSV files.
///
/// # Arguments
Expand All @@ -61,36 +65,204 @@ pub fn read_processes(
year_range: &RangeInclusive<u32>,
) -> Result<HashMap<Rc<str>, Rc<Process>>> {
let file_path = model_dir.join(PROCESSES_FILE_NAME);
let mut descriptions = read_csv_id_file::<ProcessDescription>(&file_path)?;
let descriptions = read_csv_id_file::<ProcessDescription>(&file_path)?;
let process_ids = HashSet::from_iter(descriptions.keys().cloned());

let mut availabilities = read_process_availabilities(model_dir, &process_ids, time_slice_info)?;
let mut flows = read_process_flows(model_dir, &process_ids, commodities)?;
let mut pacs = read_process_pacs(model_dir, &process_ids, commodities, &flows)?;
let mut parameters = read_process_parameters(model_dir, &process_ids, year_range)?;
let mut regions = read_process_regions(model_dir, &process_ids, region_ids)?;
let availabilities = read_process_availabilities(model_dir, &process_ids, time_slice_info)?;
let flows = read_process_flows(model_dir, &process_ids, commodities)?;
let pacs = read_process_pacs(model_dir, &process_ids, commodities, &flows)?;
let parameters = read_process_parameters(model_dir, &process_ids, year_range)?;
let regions = read_process_regions(model_dir, &process_ids, region_ids)?;

create_process_map(
descriptions.into_values(),
availabilities,
flows,
pacs,
parameters,
regions,
)
}

fn create_process_map<I>(
descriptions: I,
availabilities: GroupedMap<ProcessAvailability>,
flows: GroupedMap<ProcessFlow>,
pacs: GroupedMap<Rc<Commodity>>,
parameters: HashMap<Rc<str>, ProcessParameter>,
regions: HashMap<Rc<str>, RegionSelection>,
) -> Result<HashMap<Rc<str>, Rc<Process>>>
where
I: Iterator<Item = ProcessDescription>,
{
// Need to be mutable as we remove elements as we go along
let mut availabilities = availabilities;
let mut flows = flows;
let mut pacs = pacs;
let mut parameters = parameters;
let mut regions = regions;

Ok(process_ids
.into_iter()
.map(|id| {
// We know entry is present
let desc = descriptions.remove(&id).unwrap();
descriptions
.map(|description| {
let id = &description.id;
let availabilities = availabilities
.remove(id)
.with_context(|| format!("No availabilities defined for process {id}"))?;
let flows = flows
.remove(id)
.with_context(|| format!("No commodity flows defined for process {id}"))?;
let pacs = pacs
.remove(id)
.with_context(|| format!("No PACs defined for process {id}"))?;
Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Should we add tests for these two cases?

Copy link
Collaborator Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Yes, probably. I was being a bit lazy here.

Copy link
Collaborator Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

In order to write tests, I split out the map creation stuff into its own function. I also figured that this function is the right place to check for other missing process data (e.g. flows, availabilities). I had been checking for this by just checking that the data structures had the same length as the process_ids one, but by doing the check when we actually need the data for a given process, we can tell the user which process is missing this data, so I think that's an improvement.

let parameter = parameters
.remove(id)
.with_context(|| format!("No parameters defined for process {id}"))?;

// We've already checked that these exist for each process
let parameter = parameters.remove(&id).unwrap();
let regions = regions.remove(&id).unwrap();
// We've already checked that regions are defined for each process
let regions = regions.remove(id).unwrap();

let process = Process {
id: desc.id,
description: desc.description,
availabilities: availabilities.remove(&id).unwrap_or_default(),
flows: flows.remove(&id).unwrap_or_default(),
pacs: pacs.remove(&id).unwrap_or_default(),
id: Rc::clone(id),
description: description.description,
availabilities,
flows,
pacs,
parameter,
regions,
};

(id, process.into())
Ok((description.id, process.into()))
})
.collect())
.process_results(|iter| iter.collect())
}

#[cfg(test)]
mod tests {
use super::*;

struct ProcessData {
descriptions: Vec<ProcessDescription>,
availabilities: GroupedMap<ProcessAvailability>,
flows: GroupedMap<ProcessFlow>,
pacs: GroupedMap<Rc<Commodity>>,
parameters: HashMap<Rc<str>, ProcessParameter>,
regions: HashMap<Rc<str>, RegionSelection>,
}

/// Returns example data (without errors) for processes
fn get_process_data() -> ProcessData {
let descriptions = vec![
ProcessDescription {
id: Rc::from("process1"),
description: "Process 1".to_string(),
},
ProcessDescription {
id: Rc::from("process2"),
description: "Process 2".to_string(),
},
];

let availabilities = ["process1", "process2"]
.into_iter()
.map(|id| (id.into(), vec![]))
.collect();

let flows = ["process1", "process2"]
.into_iter()
.map(|id| (id.into(), vec![]))
.collect();

let pacs = ["process1", "process2"]
.into_iter()
.map(|id| (id.into(), vec![]))
.collect();

let parameters = ["process1", "process2"]
.into_iter()
.map(|id| {
let parameter = ProcessParameter {
process_id: id.to_string(),
years: 2010..=2020,
capital_cost: 0.0,
fixed_operating_cost: 0.0,
variable_operating_cost: 0.0,
lifetime: 1,
discount_rate: 1.0,
cap2act: 0.0,
};

(id.into(), parameter)
})
.collect();

let regions = ["process1", "process2"]
.into_iter()
.map(|id| (id.into(), RegionSelection::All))
.collect();

ProcessData {
descriptions,
availabilities,
flows,
pacs,
parameters,
regions,
}
}

#[test]
fn test_create_process_map_success() {
let data = get_process_data();
let result = create_process_map(
data.descriptions.into_iter(),
data.availabilities,
data.flows,
data.pacs,
data.parameters,
data.regions,
)
.unwrap();

assert_eq!(result.len(), 2);
assert!(result.contains_key("process1"));
assert!(result.contains_key("process2"));
}

/// Generate code for a test with data missing for a given field
macro_rules! test_missing {
($field:ident) => {
let mut data = get_process_data();
data.$field.remove("process1");

let result = create_process_map(
data.descriptions.into_iter(),
data.availabilities,
data.flows,
data.pacs,
data.parameters,
data.regions,
);
assert!(result.is_err());
};
}

#[test]
fn test_create_process_map_missing_availabilities() {
test_missing!(availabilities);
}

#[test]
fn test_create_process_map_missing_pacs() {
test_missing!(pacs);
}

#[test]
fn test_create_process_map_missing_flows() {
test_missing!(flows);
}

#[test]
fn test_create_process_map_missing_parameters() {
test_missing!(parameters);
}
}
30 changes: 11 additions & 19 deletions src/input/process/availability.rs
Original file line number Diff line number Diff line change
Expand Up @@ -3,7 +3,7 @@ use super::define_process_id_getter;
use crate::input::*;
use crate::process::{LimitType, ProcessAvailability};
use crate::time_slice::TimeSliceInfo;
use anyhow::{ensure, Context, Result};
use anyhow::{Context, Result};
use itertools::Itertools;
use serde::Deserialize;
use std::collections::{HashMap, HashSet};
Expand Down Expand Up @@ -44,23 +44,15 @@ fn read_process_availabilities_from_iter<I>(
where
I: Iterator<Item = ProcessAvailabilityRaw>,
{
let availabilities = iter
.map(|record| -> Result<_> {
let time_slice = time_slice_info.get_selection(&record.time_slice)?;

Ok(ProcessAvailability {
process_id: record.process_id,
limit_type: record.limit_type,
time_slice,
value: record.value,
})
iter.map(|record| -> Result<_> {
let time_slice = time_slice_info.get_selection(&record.time_slice)?;

Ok(ProcessAvailability {
process_id: record.process_id,
limit_type: record.limit_type,
time_slice,
value: record.value,
})
.process_results(|iter| iter.into_id_map(process_ids))??;

ensure!(
availabilities.len() >= process_ids.len(),
"Every process must have at least one availability period"
);

Ok(availabilities)
})
.process_results(|iter| iter.into_id_map(process_ids))?
}
Loading
Loading