Skip to content

Commit 7f27c71

Browse files
committed
Merge branch 'main' into prices_tidy
2 parents 16ee320 + d904081 commit 7f27c71

9 files changed

Lines changed: 381 additions & 166 deletions

File tree

schemas/input/agent_objectives.yaml

Lines changed: 4 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -17,18 +17,16 @@ fields:
1717
type: string
1818
description: The year(s) to which this entry applies
1919
notes:
20-
One or more milestone years separated by semicolons, `all` to select all years or a year
21-
range in the form 'start..end' to select all valid years within range, inclusive. Either 'start'
20+
One or more milestone years separated by semicolons, `all` to select all years or a year range
21+
in the form 'start..end' to select all valid years within range, inclusive. Either 'start'
2222
'end' or both can be omitted, which will set the corresponding limit to the minimum or maximum
2323
valid year, respectively.
2424
- name: objective_type
2525
type: string
26-
enum: [lcox, npv]
26+
enum: [npv, lcox]
2727
description: The type of objective
2828
notes: |
29-
Must be `npv` (net present value) or `lcox` (levelised cost of X). Note that support for NPV
30-
is [currently broken](https://github.com/EnergySystemsModellingLab/MUSE2/issues/716), so don't
31-
enable this option unless you know what you're doing.
29+
Must be `npv` (net present value) or `lcox` (levelised cost of X).
3230
- name: decision_weight
3331
type: number
3432
description: Weight for weighted sum decision rule

schemas/input/commodities.yaml

Lines changed: 9 additions & 11 deletions
Original file line numberDiff line numberDiff line change
@@ -30,15 +30,13 @@ fields:
3030
- name: pricing_strategy
3131
type: string
3232
enum:
33-
[
34-
shadow,
35-
marginal,
36-
marginal_average,
37-
full,
38-
full_average,
39-
scarcity,
40-
unpriced,
41-
]
33+
- shadow
34+
- marginal
35+
- marginal_average
36+
- full
37+
- full_average
38+
- scarcity
39+
- unpriced
4240
description: The pricing strategy for this commodity
4341
notes: |
4442
Optional. If specified, must be one of `shadow` (priced using shadow prices from supply-demand
@@ -49,8 +47,8 @@ fields:
4947
production across assets), `scarcity` (priced adjusted for scarcity), or `unpriced` (not
5048
priced at all).
5149
52-
For `svd` and `sed` commodities, this must be one of `shadow`, `marginal`,
53-
`marginal_average`, `full` or `full_average`. For `oth` commodities, this must be
50+
For `svd` and `sed` commodities, it must be one of `shadow`, `marginal`,
51+
`marginal_average`, `full` or `full_average`. For `oth` commodities, it *must* be
5452
`unpriced`.
5553
5654
If unspecified, the commodity will use the default pricing strategy according to the commodity

src/asset.rs

Lines changed: 93 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1100,6 +1100,41 @@ impl AssetRef {
11001100
f(Some(&self), child);
11011101
}
11021102
}
1103+
1104+
/// Get an [`AssetRef`] representing a subset of this parent's children.
1105+
///
1106+
/// # Panics
1107+
///
1108+
/// Panics if this asset is not a parent asset or `num_units` is zero or exceeds the total
1109+
/// capacity of this asset.
1110+
pub fn make_partial_parent(&self, num_units: u32) -> Self {
1111+
assert!(
1112+
self.is_parent(),
1113+
"Cannot make a partial parent from a non-parent asset"
1114+
);
1115+
assert!(
1116+
num_units > 0,
1117+
"Cannot make a partial parent with zero units"
1118+
);
1119+
1120+
let (max_num_units, unit_size) = match self.capacity() {
1121+
AssetCapacity::Discrete(max_num_units, unit_size) => (max_num_units, unit_size),
1122+
// We know asset capacity type is discrete as this is a parent asset
1123+
AssetCapacity::Continuous(_) => unreachable!(),
1124+
};
1125+
match num_units.cmp(&max_num_units) {
1126+
// Make a new Asset with fewer units
1127+
Ordering::Less => Self::from(Asset {
1128+
capacity: Cell::new(AssetCapacity::Discrete(num_units, unit_size)),
1129+
..Asset::clone(self)
1130+
}),
1131+
// Same number of units as self
1132+
Ordering::Equal => self.clone(),
1133+
Ordering::Greater => {
1134+
panic!("Cannot make a partial parent with more units than original")
1135+
}
1136+
}
1137+
}
11031138
}
11041139

11051140
impl From<Rc<Asset>> for AssetRef {
@@ -1429,6 +1464,64 @@ mod tests {
14291464
assert_eq!(count, 1);
14301465
}
14311466

1467+
#[fixture]
1468+
fn parent_asset(asset_divisible: Asset) -> AssetRef {
1469+
let asset = AssetRef::from(asset_divisible);
1470+
let mut parent = None;
1471+
1472+
asset.into_for_each_child(&mut 0, |maybe_parent, _| {
1473+
if parent.is_none() {
1474+
parent = maybe_parent.cloned();
1475+
}
1476+
});
1477+
1478+
parent.expect("Divisible asset should create a parent")
1479+
}
1480+
1481+
#[rstest]
1482+
#[case::subset_of_children(2, false)]
1483+
#[case::all_children(3, true)]
1484+
fn make_partial_parent(
1485+
parent_asset: AssetRef,
1486+
#[case] num_units: u32,
1487+
#[case] expect_same_asset: bool,
1488+
) {
1489+
let parent = parent_asset;
1490+
assert!(parent.is_parent());
1491+
1492+
let partial_parent = parent.make_partial_parent(num_units);
1493+
1494+
assert!(partial_parent.is_parent());
1495+
assert_eq!(
1496+
partial_parent.capacity(),
1497+
AssetCapacity::Discrete(num_units, Capacity(4.0))
1498+
);
1499+
assert_eq!(partial_parent.num_children(), Some(num_units));
1500+
assert_eq!(partial_parent.group_id(), parent.group_id());
1501+
assert_eq!(partial_parent.agent_id(), parent.agent_id());
1502+
assert_eq!(Rc::ptr_eq(&partial_parent.0, &parent.0), expect_same_asset);
1503+
assert_eq!(parent.capacity(), AssetCapacity::Discrete(3, Capacity(4.0)));
1504+
}
1505+
1506+
#[rstest]
1507+
#[should_panic(expected = "Cannot make a partial parent from a non-parent asset")]
1508+
fn make_partial_parent_panics_for_non_parent_asset(asset_divisible: Asset) {
1509+
let asset = AssetRef::from(asset_divisible);
1510+
asset.make_partial_parent(1);
1511+
}
1512+
1513+
#[rstest]
1514+
#[should_panic(expected = "Cannot make a partial parent with zero units")]
1515+
fn make_partial_parent_panics_for_zero_units(parent_asset: AssetRef) {
1516+
parent_asset.make_partial_parent(0);
1517+
}
1518+
1519+
#[rstest]
1520+
#[should_panic(expected = "Cannot make a partial parent with more units than original")]
1521+
fn make_partial_parent_panics_for_too_many_units(parent_asset: AssetRef) {
1522+
parent_asset.make_partial_parent(4);
1523+
}
1524+
14321525
#[rstest]
14331526
fn asset_commission(process: Process) {
14341527
// Test successful commissioning of Future asset

src/asset/capacity.rs

Lines changed: 128 additions & 12 deletions
Original file line numberDiff line numberDiff line change
@@ -13,6 +13,23 @@ pub enum AssetCapacity {
1313
Discrete(u32, Capacity),
1414
}
1515

16+
impl AssetCapacity {
17+
/// Return the smaller of `self` or `other`.
18+
///
19+
/// # Panics
20+
///
21+
/// Panics if the comparison is not meaningful. This happens if either `AssetCapacity` contains
22+
/// a NaN value, one is discrete and the other continuous or if both are discrete and the unit
23+
/// size differs.
24+
pub fn min(self, other: AssetCapacity) -> AssetCapacity {
25+
match self.partial_cmp(&other) {
26+
None => panic!("Comparing invalid AssetCapacity values ({self:?} and {other:?})"),
27+
Some(Ordering::Greater) => other,
28+
_ => self,
29+
}
30+
}
31+
}
32+
1633
impl Add for AssetCapacity {
1734
type Output = Self;
1835

@@ -49,23 +66,15 @@ impl Sub for AssetCapacity {
4966
}
5067
}
5168

52-
impl Eq for AssetCapacity {}
53-
5469
impl PartialOrd for AssetCapacity {
5570
fn partial_cmp(&self, other: &Self) -> Option<Ordering> {
56-
Some(self.cmp(other))
57-
}
58-
}
59-
60-
impl Ord for AssetCapacity {
61-
fn cmp(&self, other: &Self) -> Ordering {
6271
match (self, other) {
63-
(AssetCapacity::Continuous(a), AssetCapacity::Continuous(b)) => a.total_cmp(b),
72+
(AssetCapacity::Continuous(a), AssetCapacity::Continuous(b)) => a.partial_cmp(b),
6473
(AssetCapacity::Discrete(units1, size1), AssetCapacity::Discrete(units2, size2)) => {
65-
Self::check_same_unit_size(*size1, *size2);
66-
units1.cmp(units2)
74+
// NB: Also returns `None` if either is NaN
75+
(*size1 == *size2).then(|| units1.cmp(units2))
6776
}
68-
_ => panic!("Cannot compare different types of AssetCapacity ({self:?} and {other:?})"),
77+
_ => None,
6978
}
7079
}
7180
}
@@ -213,4 +222,111 @@ mod tests {
213222
let got = orig.apply_limit_factor(factor);
214223
assert_eq!(got, AssetCapacity::Discrete(expected_units, unit_size));
215224
}
225+
226+
#[rstest]
227+
#[case::less(
228+
AssetCapacity::Continuous(Capacity(4.0)),
229+
AssetCapacity::Continuous(Capacity(6.0)),
230+
Some(Ordering::Less)
231+
)]
232+
#[case::equal(
233+
AssetCapacity::Continuous(Capacity(4.0)),
234+
AssetCapacity::Continuous(Capacity(4.0)),
235+
Some(Ordering::Equal)
236+
)]
237+
#[case::greater(
238+
AssetCapacity::Continuous(Capacity(6.0)),
239+
AssetCapacity::Continuous(Capacity(4.0)),
240+
Some(Ordering::Greater)
241+
)]
242+
fn partial_cmp_continuous(
243+
#[case] left: AssetCapacity,
244+
#[case] right: AssetCapacity,
245+
#[case] expected: Option<Ordering>,
246+
) {
247+
assert_eq!(left.partial_cmp(&right), expected);
248+
assert_eq!(left == right, expected == Some(Ordering::Equal));
249+
}
250+
251+
#[rstest]
252+
#[case::less(
253+
AssetCapacity::Discrete(2, Capacity(3.0)),
254+
AssetCapacity::Discrete(4, Capacity(3.0)),
255+
Some(Ordering::Less)
256+
)]
257+
#[case::equal(
258+
AssetCapacity::Discrete(4, Capacity(3.0)),
259+
AssetCapacity::Discrete(4, Capacity(3.0)),
260+
Some(Ordering::Equal)
261+
)]
262+
#[case::greater(
263+
AssetCapacity::Discrete(5, Capacity(3.0)),
264+
AssetCapacity::Discrete(4, Capacity(3.0)),
265+
Some(Ordering::Greater)
266+
)]
267+
fn partial_cmp_discrete_with_matching_unit_size(
268+
#[case] left: AssetCapacity,
269+
#[case] right: AssetCapacity,
270+
#[case] expected: Option<Ordering>,
271+
) {
272+
assert_eq!(left.partial_cmp(&right), expected);
273+
assert_eq!(left == right, expected == Some(Ordering::Equal));
274+
}
275+
276+
#[rstest]
277+
#[case::mixed_types(
278+
AssetCapacity::Continuous(Capacity(4.0)),
279+
AssetCapacity::Discrete(4, Capacity(1.0))
280+
)]
281+
#[case::different_unit_sizes(
282+
AssetCapacity::Discrete(4, Capacity(1.0)),
283+
AssetCapacity::Discrete(4, Capacity(2.0))
284+
)]
285+
#[case::nan_continuous(
286+
AssetCapacity::Continuous(Capacity(f64::NAN)),
287+
AssetCapacity::Continuous(Capacity(4.0))
288+
)]
289+
fn partial_cmp_returns_none_for_invalid_comparisons(
290+
#[case] left: AssetCapacity,
291+
#[case] right: AssetCapacity,
292+
) {
293+
assert_eq!(left.partial_cmp(&right), None);
294+
assert!(left != right);
295+
}
296+
297+
#[rstest]
298+
#[case::continuous(
299+
AssetCapacity::Continuous(Capacity(4.0)),
300+
AssetCapacity::Continuous(Capacity(6.0)),
301+
AssetCapacity::Continuous(Capacity(4.0))
302+
)]
303+
#[case::discrete(
304+
AssetCapacity::Discrete(2, Capacity(3.0)),
305+
AssetCapacity::Discrete(4, Capacity(3.0)),
306+
AssetCapacity::Discrete(2, Capacity(3.0))
307+
)]
308+
fn min_returns_smaller_capacity(
309+
#[case] left: AssetCapacity,
310+
#[case] right: AssetCapacity,
311+
#[case] expected: AssetCapacity,
312+
) {
313+
assert_eq!(left.min(right), expected);
314+
}
315+
316+
#[rstest]
317+
#[case::mixed_types(
318+
AssetCapacity::Continuous(Capacity(4.0)),
319+
AssetCapacity::Discrete(4, Capacity(1.0))
320+
)]
321+
#[case::different_unit_sizes(
322+
AssetCapacity::Discrete(4, Capacity(1.0)),
323+
AssetCapacity::Discrete(4, Capacity(2.0))
324+
)]
325+
#[should_panic(expected = "Comparing invalid AssetCapacity values")]
326+
fn min_panics_for_invalid_comparisons(
327+
#[case] left: AssetCapacity,
328+
#[case] right: AssetCapacity,
329+
) {
330+
let _ = left.min(right);
331+
}
216332
}

src/simulation/investment.rs

Lines changed: 4 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -20,7 +20,7 @@ pub mod appraisal;
2020
use appraisal::coefficients::calculate_coefficients_for_assets;
2121
use appraisal::{
2222
AppraisalOutput, appraise_investment, count_equal_and_best_appraisal_outputs,
23-
sort_appraisal_outputs_by_investment_priority,
23+
sort_and_filter_appraisal_outputs,
2424
};
2525

2626
/// A map of demand across time slices for a specific market
@@ -796,16 +796,11 @@ fn select_best_assets(
796796
&demand,
797797
)?;
798798

799-
sort_appraisal_outputs_by_investment_priority(&mut outputs_for_opts);
799+
// Sort by investment priority and discord non-feasible options
800+
sort_and_filter_appraisal_outputs(&mut outputs_for_opts);
800801

801-
// Check if all options have zero capacity. If so, we cannot meet demand, so have to bail
802+
// Check if there are any remaining options. If not, we cannot meet demand, so have to bail
802803
// out.
803-
//
804-
// This may happen if:
805-
// - the asset has zero activity limits for all time slices with
806-
// demand.
807-
// - known issue with the NPV objective
808-
// (see https://github.com/EnergySystemsModellingLab/MUSE2/issues/716).
809804
if outputs_for_opts.is_empty() {
810805
let remaining_demands: Vec<_> = demand
811806
.iter()

0 commit comments

Comments
 (0)