Skip to content

Commit 8fe9861

Browse files
authored
Merge pull request #628 from EnergySystemsModellingLab/add-constraints
Add commodity balance and demand satisfaction constraints
2 parents 5ff7770 + 5c468bd commit 8fe9861

8 files changed

Lines changed: 57 additions & 49 deletions

File tree

src/asset.rs

Lines changed: 5 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -264,20 +264,15 @@ impl AssetPool {
264264
self.iter().filter(|asset| asset.region_id == *region_id)
265265
}
266266

267-
/// Iterate over only the active assets in a given region that produce or consume a given
268-
/// commodity
267+
/// Iterate over the active assets in a given region that produce/consume a commodity with the
268+
/// associated process flow
269269
pub fn iter_for_region_and_commodity<'a>(
270270
&'a self,
271271
region_id: &'a RegionID,
272272
commodity_id: &'a CommodityID,
273-
) -> impl Iterator<Item = &'a AssetRef> {
274-
self.iter_for_region(region_id).filter(|asset| {
275-
asset.process.contains_commodity_flow(
276-
commodity_id,
277-
&asset.region_id,
278-
asset.commission_year,
279-
)
280-
})
273+
) -> impl Iterator<Item = (&'a AssetRef, &'a ProcessFlow)> {
274+
self.iter_for_region(region_id)
275+
.filter_map(|asset| Some((asset, asset.get_flow(commodity_id)?)))
281276
}
282277

283278
/// Replace the active pool with new and/or already commissioned assets

src/input/commodity/demand.rs

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -187,7 +187,7 @@ fn compute_demand_maps(
187187
) -> HashMap<CommodityID, DemandMap> {
188188
let mut map = HashMap::new();
189189
for ((commodity_id, region_id, year), (level, annual_demand)) in demand.iter() {
190-
for ts_selection in time_slice_info.iter_selections_for_level(*level) {
190+
for ts_selection in time_slice_info.iter_selections_at_level(*level) {
191191
let slice_key = (
192192
commodity_id.clone(),
193193
region_id.clone(),

src/input/commodity/demand_slicing.rs

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -128,7 +128,7 @@ fn validate_demand_slices(
128128
) -> Result<()> {
129129
for (commodity, region_id) in iproduct!(svd_commodities.values(), region_ids) {
130130
time_slice_info
131-
.iter_selections_for_level(commodity.time_slice_level)
131+
.iter_selections_at_level(commodity.time_slice_level)
132132
.map(|ts_selection| {
133133
demand_slices
134134
.get(&(

src/input/process.rs

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -174,7 +174,7 @@ fn validate_commodities(
174174
}
175175
CommodityType::ServiceDemand => {
176176
for ts_selection in
177-
time_slice_info.iter_selections_for_level(commodity.time_slice_level)
177+
time_slice_info.iter_selections_at_level(commodity.time_slice_level)
178178
{
179179
validate_svd_commodity(
180180
time_slice_info,

src/output.rs

Lines changed: 0 additions & 11 deletions
Original file line numberDiff line numberDiff line change
@@ -130,17 +130,6 @@ struct CommodityBalanceDualsRow {
130130
value: f64,
131131
}
132132

133-
/// Represents the fixed asset duals data in a row of the fixed asset duals CSV file
134-
#[derive(Serialize, Deserialize, Debug, PartialEq)]
135-
struct FixedAssetDualsRow {
136-
pac: CommodityID,
137-
pac_flow: f64,
138-
commodity_id: CommodityID,
139-
commodity_flow: f64,
140-
time_slice: TimeSliceID,
141-
value: f64,
142-
}
143-
144133
/// For writing extra debug information about the model
145134
struct DebugDataWriter {
146135
commodity_balance_duals_writer: csv::Writer<File>,

src/process.rs

Lines changed: 0 additions & 15 deletions
Original file line numberDiff line numberDiff line change
@@ -48,21 +48,6 @@ pub struct Process {
4848
pub regions: HashSet<RegionID>,
4949
}
5050

51-
impl Process {
52-
/// Whether the process contains a flow for a given commodity
53-
pub fn contains_commodity_flow(
54-
&self,
55-
commodity_id: &CommodityID,
56-
region_id: &RegionID,
57-
year: u32,
58-
) -> bool {
59-
self.flows
60-
.get(&(region_id.clone(), year))
61-
.unwrap() // all regions and years are covered
62-
.contains_key(commodity_id)
63-
}
64-
}
65-
6651
/// Represents a maximum annual commodity coeff for a given process
6752
#[derive(PartialEq, Debug, Clone)]
6853
pub struct ProcessFlow {

src/simulation/optimisation/constraints.rs

Lines changed: 48 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,7 @@
11
//! Code for adding constraints to the dispatch optimisation problem.
22
use super::VariableMap;
33
use crate::asset::{AssetPool, AssetRef};
4-
use crate::commodity::CommodityID;
4+
use crate::commodity::{CommodityID, CommodityType};
55
use crate::model::Model;
66
use crate::region::RegionID;
77
use crate::time_slice::{TimeSliceID, TimeSliceInfo, TimeSliceSelection};
@@ -85,18 +85,57 @@ pub fn add_asset_constraints(
8585
/// [1]: https://energysystemsmodellinglab.github.io/MUSE_2.0/dispatch_optimisation.html#commodity-balance-constraints
8686
fn add_commodity_balance_constraints(
8787
problem: &mut Problem,
88-
_variables: &VariableMap,
89-
_model: &Model,
90-
_assets: &AssetPool,
91-
_year: u32,
88+
variables: &VariableMap,
89+
model: &Model,
90+
assets: &AssetPool,
91+
year: u32,
9292
) -> CommodityBalanceKeys {
9393
// Row offset in problem. This line **must** come before we add more constraints.
9494
let offset = problem.num_rows();
9595

96-
let keys = Vec::new();
97-
98-
// **TODO:** Add commodity balance constraints:
99-
// https://github.com/EnergySystemsModellingLab/MUSE_2.0/issues/577
96+
let mut keys = Vec::new();
97+
let mut terms = Vec::new();
98+
for (commodity_id, commodity) in model.commodities.iter() {
99+
if !matches!(
100+
commodity.kind,
101+
CommodityType::SupplyEqualsDemand | CommodityType::ServiceDemand
102+
) {
103+
continue;
104+
}
105+
106+
for region_id in model.iter_regions() {
107+
for ts_selection in model
108+
.time_slice_info
109+
.iter_selections_at_level(commodity.time_slice_level)
110+
{
111+
for (asset, flow) in assets.iter_for_region_and_commodity(region_id, commodity_id) {
112+
// If the commodity has a time slice level of season/annual, the constraint will
113+
// cover multiple time slices
114+
for (time_slice, _) in ts_selection.iter(&model.time_slice_info) {
115+
let var = variables.get(asset, time_slice);
116+
terms.push((var, flow.coeff));
117+
}
118+
}
119+
120+
// Add constraint. For SED commodities, the RHS is zero and for SVD commodities it
121+
// is the exogenous demand supplied by the user.
122+
let rhs = if commodity.kind == CommodityType::ServiceDemand {
123+
*commodity
124+
.demand
125+
.get(&(region_id.clone(), year, ts_selection.clone()))
126+
.unwrap()
127+
} else {
128+
0.0
129+
};
130+
problem.add_row(rhs..=rhs, terms.drain(..));
131+
keys.push((
132+
commodity_id.clone(),
133+
region_id.clone(),
134+
ts_selection.clone(),
135+
))
136+
}
137+
}
138+
}
100139

101140
CommodityBalanceKeys { offset, keys }
102141
}

src/time_slice.rs

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -292,7 +292,7 @@ impl TimeSliceInfo {
292292
///
293293
/// For example, if [`TimeSliceLevel::Season`] is specified, this function will return an
294294
/// iterator of [`TimeSliceSelection`]s covering each season.
295-
pub fn iter_selections_for_level(
295+
pub fn iter_selections_at_level(
296296
&self,
297297
level: TimeSliceLevel,
298298
) -> Box<dyn Iterator<Item = TimeSliceSelection> + '_> {

0 commit comments

Comments
 (0)