Skip to content

Commit 39768aa

Browse files
committed
Idea for simple implementation (not working...)
1 parent 51eb29a commit 39768aa

File tree

3 files changed

+162
-27
lines changed

3 files changed

+162
-27
lines changed

src/model/parameters.rs

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -47,7 +47,7 @@ define_unit_param_default!(default_candidate_asset_capacity, Capacity, 0.0001);
4747
define_unit_param_default!(default_capacity_limit_factor, Dimensionless, 0.1);
4848
define_unit_param_default!(default_value_of_lost_load, MoneyPerFlow, 1e9);
4949
define_unit_param_default!(default_price_tolerance, Dimensionless, 1e-6);
50-
define_param_default!(default_max_ironing_out_iterations, u32, 10);
50+
define_param_default!(default_max_ironing_out_iterations, u32, 1);
5151

5252
/// Model parameters as defined in the `model.toml` file.
5353
///

src/simulation/investment.rs

Lines changed: 151 additions & 20 deletions
Original file line numberDiff line numberDiff line change
@@ -9,11 +9,11 @@ use crate::region::RegionID;
99
use crate::simulation::CommodityPrices;
1010
use crate::time_slice::{TimeSliceID, TimeSliceInfo};
1111
use crate::units::{Capacity, Dimensionless, Flow, FlowPerCapacity};
12-
use anyhow::{Result, bail, ensure};
12+
use anyhow::{Result, ensure};
1313
use indexmap::IndexMap;
1414
use itertools::{Itertools, chain, iproduct};
15-
use log::debug;
16-
use std::collections::HashMap;
15+
use log::{debug, warn};
16+
use std::collections::{HashMap, VecDeque};
1717
use std::fmt::Display;
1818

1919
pub mod appraisal;
@@ -55,6 +55,8 @@ impl InvestmentSet {
5555
demand: &AllDemandMap,
5656
existing_assets: &[AssetRef],
5757
prices: &CommodityPrices,
58+
seen_markets: &[MarketID],
59+
previously_selected_assets: &[AssetRef],
5860
writer: &mut DataWriter,
5961
) -> Result<Vec<AssetRef>> {
6062
match self {
@@ -67,15 +69,20 @@ impl InvestmentSet {
6769
prices,
6870
writer,
6971
),
70-
InvestmentSet::Cycle(markets) => select_assets_for_cycle(
71-
model,
72-
markets,
73-
year,
74-
demand,
75-
existing_assets,
76-
prices,
77-
writer,
78-
),
72+
InvestmentSet::Cycle(markets) => {
73+
debug!("Starting investment for cycle '{self}'");
74+
select_assets_for_cycle(
75+
model,
76+
markets,
77+
year,
78+
demand,
79+
existing_assets,
80+
prices,
81+
seen_markets,
82+
previously_selected_assets,
83+
writer,
84+
)
85+
}
7986
InvestmentSet::Layer(investment_sets) => {
8087
debug!("Starting investment for layer '{self}'");
8188
let mut all_assets = Vec::new();
@@ -86,6 +93,8 @@ impl InvestmentSet {
8693
demand,
8794
existing_assets,
8895
prices,
96+
seen_markets,
97+
previously_selected_assets,
8998
writer,
9099
)?;
91100
all_assets.extend(assets);
@@ -157,7 +166,9 @@ pub fn perform_agent_investment(
157166
year,
158167
&net_demand,
159168
existing_assets,
160-
prices,
169+
&external_prices,
170+
&seen_markets,
171+
&all_selected_assets,
161172
writer,
162173
)?;
163174

@@ -277,16 +288,114 @@ fn select_assets_for_market(
277288
Ok(selected_assets)
278289
}
279290

291+
#[allow(clippy::too_many_arguments)]
280292
fn select_assets_for_cycle(
281-
_model: &Model,
293+
model: &Model,
282294
markets: &[MarketID],
283-
_year: u32,
284-
_demand: &AllDemandMap,
285-
_existing_assets: &[AssetRef],
286-
_prices: &CommodityPrices,
287-
_writer: &mut DataWriter,
295+
year: u32,
296+
demand: &AllDemandMap,
297+
existing_assets: &[AssetRef],
298+
prices: &CommodityPrices,
299+
seen_markets: &[MarketID],
300+
previously_selected_assets: &[AssetRef],
301+
writer: &mut DataWriter,
288302
) -> Result<Vec<AssetRef>> {
289-
bail!("Investment cycles are not yet supported. Found cycle for commodities: {markets:?}");
303+
// Get markets to balance: all seen so far plus all in loop
304+
let mut markets_to_balance = seen_markets.to_vec();
305+
markets_to_balance.extend_from_slice(markets);
306+
307+
// Initialise list of unbalanced markets: start with all markets in the cycle
308+
let mut unbalanced_markets = VecDeque::from(markets.to_vec());
309+
310+
// Iterate while there are markets in the unbalanced list
311+
let mut current_demand = demand.clone();
312+
let mut selected_assets = HashMap::new();
313+
let mut loop_iter = 0;
314+
loop {
315+
// If max iterations reached, break with a warning
316+
if loop_iter == 10 {
317+
warn!(
318+
"Max cycle investment iterations reached. Unable to find stable solution for the following markets: {}",
319+
unbalanced_markets.iter().join(", ")
320+
);
321+
break;
322+
}
323+
324+
// Pop off the first market
325+
// If there are no markets with unmet demand, we're done
326+
let Some(current_market) = unbalanced_markets.pop_front() else {
327+
debug!(
328+
"Cycle investment for '{}' converged after {} iterations",
329+
markets.iter().join(", "),
330+
loop_iter
331+
);
332+
break;
333+
};
334+
335+
// Select assets for this market
336+
// This will replace any previously selected assets for this market
337+
let assets = select_assets_for_market(
338+
model,
339+
&current_market,
340+
year,
341+
&current_demand,
342+
existing_assets,
343+
prices,
344+
writer,
345+
)?;
346+
selected_assets.insert(current_market.clone(), assets);
347+
348+
// Assemble full list of assets for dispatch
349+
let mut all_assets = previously_selected_assets.to_vec();
350+
let flat_selected: Vec<AssetRef> = selected_assets
351+
.values()
352+
.flat_map(|v| v.iter().cloned())
353+
.collect();
354+
all_assets.extend_from_slice(&flat_selected);
355+
356+
// Run dispatch
357+
// We allow unmet demand for all markets except the one we're currently balancing
358+
let markets_with_unmet_demand: Vec<MarketID> = markets
359+
.iter()
360+
.filter(|m| *m != &current_market)
361+
.cloned()
362+
.collect();
363+
let solution = DispatchRun::new(model, &all_assets, year)
364+
.with_market_balance_subset(&markets_to_balance)
365+
.with_unmet_demand_vars(&markets_with_unmet_demand)
366+
.run(
367+
&format!(
368+
"cycle ({}) iteration {}",
369+
markets.iter().join(", "),
370+
loop_iter
371+
),
372+
writer,
373+
)?;
374+
375+
// Find markets with unmet demand and add these to the unbalanced list if not already present
376+
let coms = solution.get_markets_with_unmet_demand();
377+
for market in coms {
378+
if !unbalanced_markets.contains(&market) {
379+
unbalanced_markets.push_back(market);
380+
}
381+
}
382+
println!(
383+
"Unbalanced markets: {}",
384+
unbalanced_markets.iter().join(", ")
385+
);
386+
387+
// Calculate a new demand map for the next iteration
388+
current_demand.clone_from(demand);
389+
update_demand_map_with_inputs(
390+
&mut current_demand,
391+
&solution.create_flow_map(),
392+
&flat_selected,
393+
);
394+
395+
loop_iter += 1;
396+
}
397+
398+
Ok(selected_assets.into_values().flatten().collect())
290399
}
291400

292401
/// Flatten the preset commodity demands for a given year into a map of commodity, region and
@@ -362,6 +471,28 @@ fn update_net_demand_map(demand: &mut AllDemandMap, flows: &FlowMap, assets: &[A
362471
}
363472
}
364473

474+
/// Update demand map with input flows from a set of assets
475+
fn update_demand_map_with_inputs(demand: &mut AllDemandMap, flows: &FlowMap, assets: &[AssetRef]) {
476+
for ((asset, commodity_id, time_slice), flow) in flows {
477+
if assets.contains(asset) {
478+
let key = (
479+
commodity_id.clone(),
480+
asset.region_id().clone(),
481+
time_slice.clone(),
482+
);
483+
484+
// Only consider input flows
485+
if flow < &Flow(0.0) {
486+
// Note: we use the negative of the flow as input flows are negative in the flow map.
487+
demand
488+
.entry(key)
489+
.and_modify(|value| *value -= *flow)
490+
.or_insert(-*flow);
491+
}
492+
}
493+
}
494+
}
495+
365496
/// Get a portion of the demand profile for this commodity and region
366497
fn get_demand_portion_for_commodity(
367498
time_slice_info: &TimeSliceInfo,

src/simulation/optimisation.rs

Lines changed: 10 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -280,6 +280,14 @@ impl Solution<'_> {
280280
keys.zip(output[variable_idx.clone()].iter())
281281
.map(|((asset, time_slice), value)| (asset, time_slice, T::new(*value)))
282282
}
283+
284+
/// Get the markets for which unment demand is positive in any time slice.
285+
pub fn get_markets_with_unmet_demand(&self) -> IndexSet<MarketID> {
286+
self.iter_unmet_demand()
287+
.filter(|(_, _, flow)| *flow > Flow(0.0))
288+
.map(|(market, _, _)| market.clone())
289+
.collect()
290+
}
283291
}
284292

285293
/// Defines the possible errors that can occur when running the solver
@@ -461,19 +469,15 @@ impl<'model, 'run> DispatchRun<'model, 'run> {
461469
}
462470

463471
/// Run dispatch to diagnose which markets have unmet demand
464-
fn get_markets_with_unmet_demand(
472+
pub fn get_markets_with_unmet_demand(
465473
&self,
466474
markets_to_balance: &[MarketID],
467475
) -> Result<IndexSet<MarketID>> {
468476
// Run dispatch including unmet demand variables for all markets being balanced
469477
let solution = self.run_internal(markets_to_balance, markets_to_balance)?;
470478

471479
// Collect markets with unmet demand
472-
Ok(solution
473-
.iter_unmet_demand()
474-
.filter(|(_, _, flow)| *flow > Flow(0.0))
475-
.map(|(market, _, _)| market.clone())
476-
.collect())
480+
Ok(solution.get_markets_with_unmet_demand())
477481
}
478482

479483
/// Run dispatch to balance the specified markets, allowing unmet demand for a subset of these

0 commit comments

Comments
 (0)