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
7 changes: 4 additions & 3 deletions benches/assets.rs
Original file line number Diff line number Diff line change
Expand Up @@ -9,7 +9,7 @@ use muse2::model::Model;
use muse2::output::DataWriter;
use muse2::process::{Process, ProcessID};
use muse2::simulation::candidate_assets_for_next_year;
use muse2::simulation::investment::{collect_preset_demands_for_year, select_best_assets};
use muse2::simulation::investment::{flatten_preset_demands_for_year, select_best_assets};
use muse2::simulation::market::{
collect_agent_limits, get_asset_options, get_demand_portion_for_market, get_responsible_agents,
};
Expand Down Expand Up @@ -154,11 +154,12 @@ fn criterion_benchmark(c: &mut Criterion) {
.next()
.expect("No agent found responsible for the target commodity/region/year");

let net_demand = collect_preset_demands_for_year(&model.commodities, YEAR);
let net_demand =
flatten_preset_demands_for_year(&model.commodities, &model.time_slice_info, YEAR);
let demand = get_demand_portion_for_market(
&model.time_slice_info,
&net_demand,
commodity,
&commodity.id,
region_id,
commodity_portion,
);
Expand Down
4 changes: 2 additions & 2 deletions src/fixture.rs
Original file line number Diff line number Diff line change
Expand Up @@ -19,7 +19,7 @@ use crate::simulation::investment::appraisal::LCOXMetric;
use crate::simulation::investment::appraisal::{
AppraisalOutput, coefficients::ObjectiveCoefficients,
};
use crate::time_slice::{TimeSliceID, TimeSliceInfo, TimeSliceLevel, TimeSliceSelection};
use crate::time_slice::{TimeSliceID, TimeSliceInfo, TimeSliceLevel};
use crate::units::{
Activity, ActivityPerCapacity, Capacity, Dimensionless, Flow, MoneyPerActivity,
MoneyPerCapacity, MoneyPerCapacityPerYear, Year,
Expand Down Expand Up @@ -406,7 +406,7 @@ pub fn appraisal_output(asset: Asset, time_slice: TimeSliceID) -> AppraisalOutpu
let activity_coefficients = indexmap! { time_slice.clone() => MoneyPerActivity(0.5) };
let market_costs = indexmap! { time_slice.clone() => MoneyPerActivity(0.4) };
let activity = indexmap! { time_slice.clone() => Activity(10.0) };
let unmet_demand = indexmap! { TimeSliceSelection::Single(time_slice.clone()) => Flow(5.0) };
let unmet_demand = indexmap! { time_slice.clone() => Flow(5.0) };
AppraisalOutput {
asset: AssetRef::from(asset),
coefficients: Arc::new(ObjectiveCoefficients {
Expand Down
36 changes: 13 additions & 23 deletions src/output.rs
Original file line number Diff line number Diff line change
Expand Up @@ -7,7 +7,7 @@ use crate::region::RegionID;
use crate::simulation::investment::appraisal::AppraisalOutput;
use crate::simulation::optimisation::{FlowMap, Solution};
use crate::simulation::prices::PriceMap;
use crate::time_slice::{TimeSliceID, TimeSliceLevel, TimeSliceSelection};
use crate::time_slice::TimeSliceID;
use crate::units::{Activity, Capacity, Flow, Money, MoneyPerActivity, MoneyPerFlow};
use anyhow::{Context, Result, ensure};
use csv;
Expand Down Expand Up @@ -257,9 +257,8 @@ struct AppraisalResultsTimeSliceRow {
time_slice: TimeSliceID,
activity: Activity,
activity_coefficient: MoneyPerActivity,
time_slice_level: TimeSliceLevel,
demand_for_selection: Flow,
unmet_demand_for_selection: Flow,
demand: Flow,
unmet_demand: Flow,
}

/// For writing extra debug information about the model
Expand Down Expand Up @@ -503,17 +502,13 @@ impl DebugDataWriter {
milestone_year: u32,
run_description: &str,
appraisal_results: &[AppraisalOutput],
demand: &IndexMap<TimeSliceSelection, Flow>,
time_slice_level: TimeSliceLevel,
demand: &IndexMap<TimeSliceID, Flow>,
) -> Result<()> {
for result in appraisal_results {
for (time_slice, activity) in &result.activity {
let activity_coefficient = result.coefficients.activity_coefficients[time_slice];
// Map the individual time slice back to its containing selection so we can look
// up selection-level demand and unmet demand.
let selection = time_slice_level.containing_selection(time_slice);
let demand = demand[&selection];
let unmet_demand = result.unmet_demand[&selection];
let demand = demand[time_slice];
let unmet_demand = result.unmet_demand[time_slice];
let row = AppraisalResultsTimeSliceRow {
milestone_year,
run_description: self.with_context(run_description),
Expand All @@ -523,9 +518,8 @@ impl DebugDataWriter {
time_slice: time_slice.clone(),
activity: *activity,
activity_coefficient,
time_slice_level,
demand_for_selection: demand,
unmet_demand_for_selection: unmet_demand,
demand,
unmet_demand,
};
self.appraisal_results_time_slice_writer.serialize(row)?;
}
Expand Down Expand Up @@ -610,8 +604,7 @@ impl DataWriter {
milestone_year: u32,
run_description: &str,
appraisal_results: &[AppraisalOutput],
demand: &IndexMap<TimeSliceSelection, Flow>,
time_slice_level: TimeSliceLevel,
demand: &IndexMap<TimeSliceID, Flow>,
) -> Result<()> {
if let Some(wtr) = &mut self.debug {
wtr.write_appraisal_results(milestone_year, run_description, appraisal_results)?;
Expand All @@ -620,7 +613,6 @@ impl DataWriter {
run_description,
appraisal_results,
demand,
time_slice_level,
)?;
}

Expand Down Expand Up @@ -725,7 +717,7 @@ mod tests {
appraisal_output, asset, assets, commodity_id, multi_tranche_asset, region_id, time_slice,
};
use crate::simulation::investment::appraisal::AppraisalOutput;
use crate::time_slice::{TimeSliceID, TimeSliceLevel, TimeSliceSelection};
use crate::time_slice::TimeSliceID;
use indexmap::indexmap;
use itertools::{Itertools, assert_equal};
use rstest::rstest;
Expand Down Expand Up @@ -1135,7 +1127,7 @@ mod tests {
let milestone_year = 2020;
let run_description = "test_run".to_string();
let dir = tempdir().unwrap();
let demand = indexmap! {TimeSliceSelection::Single(time_slice.clone()) => Flow(100.0) };
let demand = indexmap! {time_slice.clone() => Flow(100.0) };

// Write appraisal time slice results
{
Expand All @@ -1146,7 +1138,6 @@ mod tests {
&run_description,
&[appraisal_output],
&demand,
TimeSliceLevel::DayNight,
)
.unwrap();
writer.flush().unwrap();
Expand All @@ -1162,9 +1153,8 @@ mod tests {
time_slice: time_slice.clone(),
activity: Activity(10.0),
activity_coefficient: MoneyPerActivity(0.5),
time_slice_level: TimeSliceLevel::DayNight,
demand_for_selection: Flow(100.0),
unmet_demand_for_selection: Flow(5.0),
demand: Flow(100.0),
unmet_demand: Flow(5.0),
};
let records: Vec<AppraisalResultsTimeSliceRow> =
csv::Reader::from_path(dir.path().join(APPRAISAL_RESULTS_TIME_SLICE_FILE_NAME))
Expand Down
87 changes: 52 additions & 35 deletions src/simulation/investment.rs
Original file line number Diff line number Diff line change
Expand Up @@ -8,9 +8,9 @@ use crate::output::DataWriter;
use crate::process::ProcessID;
use crate::region::RegionID;
use crate::simulation::prices::Prices;
use crate::time_slice::{TimeSliceInfo, TimeSliceLevel, TimeSliceSelection};
use crate::time_slice::{TimeSliceID, TimeSliceInfo, TimeSliceLevel, TimeSliceSelection};
use crate::timeit::InvestmentTimer;
use crate::units::{ActivityPerCapacity, Capacity, Flow, FlowPerCapacity};
use crate::units::{ActivityPerCapacity, Capacity, Dimensionless, Flow, FlowPerCapacity};
use anyhow::{Result, ensure};
use context_manager;
use indexmap::IndexMap;
Expand All @@ -27,11 +27,11 @@ use appraisal::{
sort_and_filter_appraisal_outputs,
};

/// A map of demand across time-slice selections for a specific market
pub type DemandMap = IndexMap<TimeSliceSelection, Flow>;
/// A map of demand across time slices for a specific market
pub type DemandMap = IndexMap<TimeSliceID, Flow>;

/// Demand for a given combination of commodity, region and time-slice selection
pub type AllDemandMap = IndexMap<(CommodityID, RegionID, TimeSliceSelection), Flow>;
/// Demand for a given combination of commodity, region and time slice
pub type AllDemandMap = IndexMap<(CommodityID, RegionID, TimeSliceID), Flow>;

/// Perform agent investment to determine capacity investment of new assets for next milestone year.
///
Expand All @@ -56,7 +56,8 @@ pub fn perform_agent_investment(
writer: &mut DataWriter,
) -> Result<Vec<AssetRef>> {
// Initialise net demand map
let mut net_demand = collect_preset_demands_for_year(&model.commodities, year);
let mut net_demand =
flatten_preset_demands_for_year(&model.commodities, &model.time_slice_info, year);

// Keep a list of all the assets selected
// This includes Commissioned assets that are selected for retention, and new Ready assets
Expand Down Expand Up @@ -127,26 +128,39 @@ pub fn perform_agent_investment(
Ok(all_selected_assets)
}

/// Collect the preset commodity demands for a given year into a map of commodity, region and
/// time slice selection to demand.
/// Flatten the preset commodity demands for a given year into a map of commodity, region and
/// time slice to demand.
///
/// Demand for each commodity is stored at its natural time-slice selection level, matching the
/// balance level at which the investment appraisal operates.
pub fn collect_preset_demands_for_year(commodities: &CommodityMap, year: u32) -> AllDemandMap {
/// Since demands for some commodities may be specified at a coarser time slice level, we need to
/// distribute these demands over all time slices. Note: the way that we do this distribution is
/// irrelevant, as demands will only be balanced to the appropriate level, but we still need to do
/// this for the solver to work.
///
/// **TODO**: these assumptions may need to be revisited, e.g. when we come to storage technologies
pub fn flatten_preset_demands_for_year(
commodities: &CommodityMap,
time_slice_info: &TimeSliceInfo,
year: u32,
) -> AllDemandMap {
let mut demand_map = AllDemandMap::new();
for (commodity_id, commodity) in commodities {
for ((region_id, data_year, time_slice_selection), demand) in &commodity.demand {
if *data_year != year {
continue;
}
demand_map.insert(
(
commodity_id.clone(),
region_id.clone(),
time_slice_selection.clone(),
),
*demand,
);

// We split the demand equally over all time slices in the selection
// NOTE: since demands will only be balanced to the time slice level of the commodity
// it doesn't matter how we do this distribution, only the total matters.
#[allow(clippy::cast_precision_loss)]
let n_time_slices = time_slice_selection.iter(time_slice_info).count() as f64;
let demand_per_slice = *demand / Dimensionless(n_time_slices);
for (time_slice, _) in time_slice_selection.iter(time_slice_info) {
demand_map.insert(
(commodity_id.clone(), region_id.clone(), time_slice.clone()),
demand_per_slice,
);
}
}
}
demand_map
Expand All @@ -164,20 +178,19 @@ pub fn collect_preset_demands_for_year(commodities: &CommodityMap, year: u32) ->
pub fn update_net_demand_map(demand: &mut AllDemandMap, flows: &FlowMap, assets: &[AssetRef]) {
for ((asset, commodity_id, time_slice), flow) in flows {
if assets.contains(asset) {
let key = (
commodity_id.clone(),
asset.region_id().clone(),
time_slice.clone(),
);

// Only consider input flows and output flows from the primary output commodity
// (excluding secondary outputs)
if (flow < &Flow(0.0))
|| asset
.primary_output()
.is_some_and(|p| &p.commodity.id == commodity_id)
{
let level = asset
.get_flow(commodity_id)
.unwrap()
.commodity
.time_slice_level;
let selection = level.containing_selection(time_slice);
let key = (commodity_id.clone(), asset.region_id().clone(), selection);
// Note: we use the negative of the flow as input flows are negative in the flow map.
demand
.entry(key)
Expand Down Expand Up @@ -281,7 +294,12 @@ pub fn get_demand_limiting_capacity(
> ActivityPerCapacity(0.0)
})
})
.map(|(bucket, _)| demand[&bucket])
.map(|(bucket, _)| {
bucket
.iter(time_slice_info)
.map(|(ts, _)| demand[ts])
.sum::<Flow>()
})
.sum()
});

Expand Down Expand Up @@ -418,7 +436,6 @@ pub fn select_best_assets(
&format!("{} {} round {}", commodity.id, agent.id, round),
&outputs,
&demand,
commodity.time_slice_level,
)?;

// Sort by investment priority and discard non-feasible options
Expand Down Expand Up @@ -645,7 +662,7 @@ mod tests {
let asset = asset(process);

// Create demand map - demand of 10.0 for our time slice
let demand = indexmap! { TimeSliceSelection::Single(time_slice.clone()) => Flow(10.0)};
let demand = indexmap! { time_slice.clone() => Flow(10.0)};

// Call the function
let result = get_demand_limiting_capacity(&time_slice_info, &asset, &commodity_rc, &demand);
Expand Down Expand Up @@ -689,8 +706,8 @@ mod tests {

// Create demand map with different demands for each time slice
let demand = indexmap! {
TimeSliceSelection::Single(time_slice1.clone()) => Flow(4.0), // Requires capacity of 4.0/0.2 = 20.0
TimeSliceSelection::Single(time_slice2.clone()) => Flow(3.0), // Would require infinite capacity, but should be skipped
time_slice1.clone() => Flow(4.0), // Requires capacity of 4.0/0.2 = 20.0
time_slice2.clone() => Flow(3.0), // Would require infinite capacity, but should be skipped
};

// Call the function
Expand Down Expand Up @@ -756,8 +773,8 @@ mod tests {
let asset = asset(process);

let demand = indexmap! {
TimeSliceSelection::Single(time_slice1.clone()) => Flow(5.0),
TimeSliceSelection::Single(time_slice2.clone()) => Flow(5.0),
time_slice1.clone() => Flow(5.0),
time_slice2.clone() => Flow(5.0),
};

let result =
Expand Down Expand Up @@ -797,7 +814,7 @@ mod tests {
process.activity_limits = process_activity_limits_map(process.regions.clone(), limits);

let asset = asset(process);
let demand = indexmap! { TimeSliceSelection::Single(time_slice)=> demand_value };
let demand = indexmap! { time_slice => demand_value };
assert_eq!(
calculate_candidate_asset_capacity_scale(&asset, &commodity_rc, &demand),
expected
Expand Down
14 changes: 8 additions & 6 deletions src/simulation/investment/appraisal/constraints.rs
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,7 @@ use super::optimisation::Variable;
use crate::asset::AssetRef;
use crate::commodity::Commodity;
use crate::time_slice::{TimeSliceID, TimeSliceInfo};
use crate::units::Flow;
use highs::RowProblem as Problem;
use indexmap::IndexMap;

Expand Down Expand Up @@ -48,12 +49,13 @@ pub fn add_demand_constraints(
activity_vars: &IndexMap<TimeSliceID, Variable>,
) {
for ts_selection in time_slice_info.iter_selections_at_level(commodity.time_slice_level) {
let demand_for_ts_selection = demand[&ts_selection];
let flow_coeff = asset.get_flow(&commodity.id).unwrap().coeff;
let terms: Vec<_> = ts_selection
.iter(time_slice_info)
.map(|(time_slice, _)| (activity_vars[time_slice], flow_coeff.value()))
.collect();
let mut demand_for_ts_selection = Flow(0.0);
let mut terms = Vec::new();
for (time_slice, _) in ts_selection.iter(time_slice_info) {
demand_for_ts_selection += demand[time_slice];
let flow_coeff = asset.get_flow(&commodity.id).unwrap().coeff;
terms.push((activity_vars[time_slice], flow_coeff.value()));
}
problem.add_row(0.0..=demand_for_ts_selection.value(), terms);
}
}
Loading
Loading