Skip to content

Commit a36960c

Browse files
committed
Fix issue with prices
1 parent e48c9aa commit a36960c

File tree

2 files changed

+34
-44
lines changed

2 files changed

+34
-44
lines changed

src/simulation/investment.rs

Lines changed: 3 additions & 20 deletions
Original file line numberDiff line numberDiff line change
@@ -11,7 +11,7 @@ use crate::time_slice::{TimeSliceID, TimeSliceInfo};
1111
use crate::units::{Capacity, Dimensionless, Flow, FlowPerCapacity};
1212
use anyhow::{Result, ensure};
1313
use indexmap::IndexMap;
14-
use itertools::{Itertools, chain, iproduct};
14+
use itertools::{Itertools, chain};
1515
use log::{debug, warn};
1616
use std::collections::{HashMap, VecDeque};
1717
use std::fmt::Display;
@@ -148,11 +148,6 @@ pub fn perform_agent_investment(
148148
investment_order.iter().join(" -> ")
149149
);
150150

151-
// External prices to be used in dispatch optimisation
152-
// Once investments are performed for a commodity, the dispatch system will be able to produce
153-
// endogenous prices for that commodity, so we'll gradually remove these external prices.
154-
let mut external_prices = prices.clone();
155-
156151
// Keep track of the markets that have been seen so far. This will be used to apply
157152
// balance constraints in the dispatch optimisation - we only apply balance constraints for
158153
// commodities that have been seen so far.
@@ -166,7 +161,7 @@ pub fn perform_agent_investment(
166161
year,
167162
&net_demand,
168163
existing_assets,
169-
&external_prices,
164+
prices,
170165
&seen_markets,
171166
&all_selected_assets,
172167
writer,
@@ -177,18 +172,6 @@ pub fn perform_agent_investment(
177172
seen_markets.push(market.clone());
178173
}
179174

180-
// Remove prices for already-seen markets. Markets which are produced by at
181-
// least one asset in the dispatch run will have prices produced endogenously (via the
182-
// commodity balance constraints), but markets for which investment has not yet been
183-
// performed will, by definition, not have any producers. For these, we provide prices
184-
// from the previous dispatch run otherwise they will appear to be free to the model.
185-
for (market, time_slice) in iproduct!(
186-
investment_set.iter_markets(),
187-
model.time_slice_info.iter_ids()
188-
) {
189-
external_prices.remove(&market.commodity_id, &market.region_id, time_slice);
190-
}
191-
192175
// If no assets have been selected, skip dispatch optimisation
193176
// **TODO**: this probably means there's no demand for the commodity, which we could
194177
// presumably preempt
@@ -209,7 +192,7 @@ pub fn perform_agent_investment(
209192
// their prices using previous values so that they don't appear free
210193
let solution = DispatchRun::new(model, &all_selected_assets, year)
211194
.with_market_balance_subset(&seen_markets)
212-
.with_input_prices(&external_prices)
195+
.with_input_prices(prices)
213196
.run(&format!("post {investment_set} investment"), writer)?;
214197

215198
// Update demand map with flows from newly added assets

src/simulation/optimisation.rs

Lines changed: 31 additions & 24 deletions
Original file line numberDiff line numberDiff line change
@@ -324,23 +324,21 @@ pub fn solve_optimal(model: highs::Model) -> Result<highs::SolvedModel, ModelErr
324324
}
325325
}
326326

327-
/// Sanity check for input prices.
328-
///
329-
/// Input prices should only be provided for commodities for which there will be no commodity
330-
/// balance constraint.
331-
fn check_input_prices(input_prices: &CommodityPrices, markets: &[MarketID]) {
332-
let markets_set: HashSet<_> = markets.iter().collect();
333-
let has_prices_for_market_subset = input_prices.keys().any(|(commodity_id, region_id, _)| {
334-
let market_for_commodity_region = MarketID {
335-
commodity_id: commodity_id.clone(),
336-
region_id: region_id.clone(),
337-
};
338-
markets_set.contains(&market_for_commodity_region)
339-
});
340-
assert!(
341-
!has_prices_for_market_subset,
342-
"Input prices were included for commodities that are being modelled, which is not allowed."
343-
);
327+
/// Select prices for commodities not being balanced
328+
fn select_input_prices(
329+
input_prices: &CommodityPrices,
330+
markets_to_balance: &[MarketID],
331+
) -> CommodityPrices {
332+
let commodity_regions_set: HashSet<(&CommodityID, &RegionID)> = markets_to_balance
333+
.iter()
334+
.map(|m| (&m.commodity_id, &m.region_id))
335+
.collect();
336+
input_prices
337+
.iter()
338+
.filter(|(commodity_id, region_id, _, _)| {
339+
!commodity_regions_set.contains(&(commodity_id, region_id))
340+
})
341+
.collect()
344342
}
345343

346344
/// Provides the interface for running the dispatch optimisation.
@@ -438,18 +436,25 @@ impl<'model, 'run> DispatchRun<'model, 'run> {
438436
} else {
439437
self.markets_to_balance
440438
};
441-
if let Some(input_prices) = self.input_prices {
442-
check_input_prices(input_prices, markets_to_balance);
443-
}
439+
440+
// Select prices for commodities not being balanced
441+
let input_prices_owned = self
442+
.input_prices
443+
.map(|prices| select_input_prices(prices, markets_to_balance));
444+
let input_prices = input_prices_owned.as_ref();
444445

445446
// Try running dispatch. If it fails because the model is infeasible, it is likely that this
446447
// is due to unmet demand, in this case, we rerun dispatch including with unmet demand
447448
// variables for all markets so we can report the offending markets to users
448-
match self.run_internal(markets_to_balance, self.markets_to_allow_unmet_demand) {
449+
match self.run_internal(
450+
markets_to_balance,
451+
self.markets_to_allow_unmet_demand,
452+
input_prices,
453+
) {
449454
Ok(solution) => Ok(solution),
450455
Err(ModelError::NonOptimal(HighsModelStatus::Infeasible)) => {
451456
let markets = self
452-
.get_markets_with_unmet_demand(markets_to_balance)
457+
.get_markets_with_unmet_demand(markets_to_balance, input_prices)
453458
.expect("Failed to run dispatch to calculate unmet demand");
454459

455460
ensure!(
@@ -472,9 +477,10 @@ impl<'model, 'run> DispatchRun<'model, 'run> {
472477
pub fn get_markets_with_unmet_demand(
473478
&self,
474479
markets_to_balance: &[MarketID],
480+
input_prices: Option<&CommodityPrices>,
475481
) -> Result<IndexSet<MarketID>> {
476482
// Run dispatch including unmet demand variables for all markets being balanced
477-
let solution = self.run_internal(markets_to_balance, markets_to_balance)?;
483+
let solution = self.run_internal(markets_to_balance, markets_to_balance, input_prices)?;
478484

479485
// Collect markets with unmet demand
480486
Ok(solution.get_markets_with_unmet_demand())
@@ -485,13 +491,14 @@ impl<'model, 'run> DispatchRun<'model, 'run> {
485491
&self,
486492
markets_to_balance: &[MarketID],
487493
markets_to_allow_unmet_demand: &[MarketID],
494+
input_prices: Option<&CommodityPrices>,
488495
) -> Result<Solution<'model>, ModelError> {
489496
// Set up problem
490497
let mut problem = Problem::default();
491498
let mut variables = VariableMap::new_with_asset_vars(
492499
&mut problem,
493500
self.model,
494-
self.input_prices,
501+
input_prices,
495502
self.existing_assets,
496503
self.candidate_assets,
497504
self.year,

0 commit comments

Comments
 (0)