Skip to content

Tidy prices module#1212

Merged
tsmbland merged 10 commits intomainfrom
prices_tidy
Mar 24, 2026
Merged

Tidy prices module#1212
tsmbland merged 10 commits intomainfrom
prices_tidy

Conversation

@tsmbland
Copy link
Copy Markdown
Collaborator

@tsmbland tsmbland commented Mar 19, 2026

Description

I think this is a bit tidier. Less repetitive at least. The main change is to break the core logic for cost-based price calculation into three re-usable function:

  • iter_existing_asset_max_prices: calculates prices from existing assets based on marginal/full cost, taking the max across assets
  • iter_candidate_asset_min_prices: calculates prices from candidate assets based on marginal/full cost, taking the min across assets, and skipping any prices already covered by existing assets
  • iter_existing_asset_average_prices: calculates prices from existing assets based on marginal/full cost, taking the average across assets

I've gone for the approach suggested by @alexdewar in #1196 of passing an optional annual activities map to signal the type of cost calculation, but also passing PricingStrategy as an argument just to be extra explicit.

There's still some similarity between these three functions, but I think they are are sufficiently different that any attempts to combine them further would probably be counterproductive.

Also:

  • removing some outdated references to shadow prices in the marginal/full cost calculations.
  • I've gone for something similar to what @alexdewar suggested in Add price strategies using load-weighted averaging #1205 about adding a type parameter to WeightedAverageAccumulator. I don't know if this is necessarily improvement so let me know what you think.
  • the file is getting quite big so I was thinking of maybe breaking it up to have a strategies submodule to store the core price calculation logic, but I haven't done this here

Fixes # (issue)

Type of change

  • Bug fix (non-breaking change to fix an issue)
  • New feature (non-breaking change to add functionality)
  • Refactoring (non-breaking, non-functional change to improve maintainability)
  • Optimization (non-breaking change to speed up the code)
  • Breaking change (whatever its nature)
  • Documentation (improve or add documentation)

Key checklist

  • All tests pass: $ cargo test
  • The documentation builds and looks OK: $ cargo doc
  • Update release notes for the latest release if this PR adds a new feature or fixes a bug
    present in the previous release

Further checks

  • Code is commented, particularly in hard-to-understand areas
  • Tests added that prove fix is effective or that feature works

@tsmbland tsmbland changed the title Prices tidy Tidy prices module Mar 19, 2026
@codecov
Copy link
Copy Markdown

codecov bot commented Mar 19, 2026

Codecov Report

❌ Patch coverage is 93.45794% with 14 lines in your changes missing coverage. Please review.
✅ Project coverage is 89.74%. Comparing base (d904081) to head (b6364c7).
⚠️ Report is 12 commits behind head on main.

Files with missing lines Patch % Lines
src/simulation/prices.rs 93.45% 13 Missing and 1 partial ⚠️
Additional details and impacted files
@@            Coverage Diff             @@
##             main    #1212      +/-   ##
==========================================
+ Coverage   89.35%   89.74%   +0.39%     
==========================================
  Files          57       57              
  Lines        8263     8195      -68     
  Branches     8263     8195      -68     
==========================================
- Hits         7383     7355      -28     
+ Misses        582      544      -38     
+ Partials      298      296       -2     

☔ View full report in Codecov by Sentry.
📢 Have feedback on the report? Share it here.

🚀 New features to boost your workflow:
  • ❄️ Test Analytics: Detect flaky tests, report on failures, and find test suite problems.

@tsmbland tsmbland requested a review from Copilot March 19, 2026 16:10
@tsmbland tsmbland marked this pull request as ready for review March 19, 2026 16:10
Copy link
Copy Markdown
Contributor

Copilot AI left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Pull request overview

Refactors src/simulation/prices.rs to reduce duplication by extracting shared logic for cost-based price calculation into reusable helper iterators, and updates documentation/tests to remove outdated “shadow price” wording for marginal/full cost pricing.

Changes:

  • Extracts reusable helpers for computing selection-level prices from existing assets (max/average) and candidate assets (min).
  • Reworks marginal/full cost and average-price paths to use these helpers, then expands selection-level prices to time-slice prices.
  • Updates docstrings/tests terminology from “shadow price” to “price” where appropriate.

💡 Add Copilot custom instructions for smarter, more guided reviews. Learn how to get started.

You can also share your feedback on Copilot code review. Take the survey.

Base automatically changed from average_prices to main March 20, 2026 11:21
Copilot AI review requested due to automatic review settings March 20, 2026 11:48
Copy link
Copy Markdown
Contributor

Copilot AI left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Pull request overview

Refactors src/simulation/prices.rs to reduce repetition in cost-based pricing by extracting reusable iterator helpers for computing selection-level prices (max/avg over existing assets, min over candidates), while also cleaning up outdated “shadow price” references in docs/tests.

Changes:

  • Extracts shared pricing logic into iter_existing_asset_max_prices, iter_candidate_asset_min_prices, and iter_existing_asset_average_prices.
  • Rewires marginal/full cost pricing functions to use the new helpers and then expand selection-level prices back to per-timeslice entries.
  • Updates documentation/tests terminology from “shadow price” to “price” where the cost-based strategies use existing prices as inputs.

💡 Add Copilot custom instructions for smarter, more guided reviews. Learn how to get started.

@tsmbland tsmbland requested review from Aurashk and alexdewar March 20, 2026 12:08
@alexdewar alexdewar added this to MUSE Mar 24, 2026
@alexdewar alexdewar moved this to 👀 In review in MUSE Mar 24, 2026
Copy link
Copy Markdown
Collaborator

@alexdewar alexdewar left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

LGTM! I've made some small comments. I don't have any ideas about how to simplify this further. Maybe this is the sweet spot 😄

I actually like having the type param in WeightedAverageAccumulator. I feel like it makes sense to be explicit about the type of thing we're accumulating and to enforce it.

#[test]
fn weighted_average_accumulator_different_weights() {
let mut accum = WeightedAverageAccumulator::default();
let mut accum = WeightedAverageAccumulator::<Dimensionless>::default();
Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

You could make the type param for WeightedAverageAccumulator default to Dimensionless and then you could write this more tersely, but maybe it's good to be explicit.

}

/// Weighted average accumulator with a backup weighting path for `MoneyPerFlow` prices.
#[derive(Clone, Copy, Debug, Default)]
Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Weird that this doesn't work with the type param... not sure I understand why.

Copilot AI review requested due to automatic review settings March 24, 2026 16:12
@tsmbland tsmbland enabled auto-merge March 24, 2026 16:13
Copy link
Copy Markdown
Contributor

Copilot AI left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Pull request overview

Copilot reviewed 1 out of 3 changed files in this pull request and generated 4 comments.


💡 Add Copilot custom instructions for smarter, more guided reviews. Learn how to get started.

Comment on lines +571 to +579
PricingStrategy::MarginalCost => assert!(
annual_activities.is_none(),
"Cannot provide annual_activities with marginal pricing strategy"
),
PricingStrategy::FullCost => assert!(
annual_activities.is_some(),
"annual_activities must be provided for full pricing strategy"
),
_ => panic!("Invalid pricing strategy"),
Copy link

Copilot AI Mar 24, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Using assert!/panic! for pricing-strategy validation can crash production runs (these functions are used in simulation paths). Prefer returning an error (e.g., change the helper to return Result<impl Iterator<...>> / collect into a Result<IndexMap<...>>), or if you want to keep the API shape, use debug_assert! plus an explicit early-return behavior (e.g., return an empty iterator) with a logged error. At minimum, replace the panic!(\"Invalid pricing strategy\") with unreachable!() only if the type system guarantees other variants are impossible here; otherwise return/propagate an error.

Suggested change
PricingStrategy::MarginalCost => assert!(
annual_activities.is_none(),
"Cannot provide annual_activities with marginal pricing strategy"
),
PricingStrategy::FullCost => assert!(
annual_activities.is_some(),
"annual_activities must be provided for full pricing strategy"
),
_ => panic!("Invalid pricing strategy"),
PricingStrategy::MarginalCost => {
// In debug builds, enforce that no annual activities are provided.
debug_assert!(
annual_activities.is_none(),
"Cannot provide annual_activities with marginal pricing strategy"
);
// In release builds, if the invariant is violated, log and return an empty iterator.
if annual_activities.is_some() {
eprintln!(
"iter_existing_asset_max_prices: annual_activities must not be provided \
when using MarginalCost pricing strategy; returning empty iterator"
);
return std::iter::empty();
}
}
PricingStrategy::FullCost => {
// In debug builds, enforce that annual activities are provided.
debug_assert!(
annual_activities.is_some(),
"annual_activities must be provided for full pricing strategy"
);
// In release builds, if the invariant is violated, log and return an empty iterator.
if annual_activities.is_none() {
eprintln!(
"iter_existing_asset_max_prices: annual_activities must be provided \
when using FullCost pricing strategy; returning empty iterator"
);
return std::iter::empty();
}
}
_ => {
// This function currently only supports MarginalCost and FullCost pricing strategies.
debug_assert!(
false,
"Invalid or unsupported pricing strategy for iter_existing_asset_max_prices"
);
eprintln!(
"iter_existing_asset_max_prices: unsupported pricing strategy; returning empty iterator"
);
return std::iter::empty();
}

Copilot uses AI. Check for mistakes.

// When using full cost pricing, skip assets with zero activity across the year, since
// we cannot calculate a fixed cost per flow.
let annual_activity = annual_activities.map(|activities| activities[asset]);
Copy link

Copilot AI Mar 24, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Indexing activities[asset] will panic if annual_activities is missing an entry for any asset emitted by activity_for_existing. Since this is runtime data-dependent, consider using activities.get(asset) and either (a) skip the asset with a clear message, or (b) expect with a more actionable panic message that includes the asset id/region to aid debugging.

Suggested change
let annual_activity = annual_activities.map(|activities| activities[asset]);
let annual_activity = annual_activities.map(|activities| {
*activities
.get(asset)
.unwrap_or_else(|| {
panic!(
"Missing annual activity for asset {:?} in region {:?} when using full cost pricing",
asset,
region_id
)
})
});

Copilot uses AI. Check for mistakes.
Comment on lines +695 to +698
assert!(matches!(
pricing_strategy,
PricingStrategy::MarginalCost | PricingStrategy::FullCost
));
Copy link

Copilot AI Mar 24, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This assert! has no message, which makes failures harder to diagnose. Add an explicit message indicating the invalid pricing_strategy value and where it came from, or avoid panicking by returning an error as suggested elsewhere.

Suggested change
assert!(matches!(
pricing_strategy,
PricingStrategy::MarginalCost | PricingStrategy::FullCost
));
assert!(
matches!(
pricing_strategy,
PricingStrategy::MarginalCost | PricingStrategy::FullCost
),
"Unsupported pricing_strategy {:?} in iter_candidate_asset_min_prices for year {}",
pricing_strategy,
year
);

Copilot uses AI. Check for mistakes.
// Input-stage validation should ensure that this limit is never zero
let annual_fixed_costs_per_flow =
annual_fixed_costs.entry(asset.clone()).or_insert_with(|| {
asset.get_annual_fixed_costs_per_flow(annual_activity_limit.unwrap())
Copy link

Copilot AI Mar 24, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The unwrap() here will panic if annual_activity_limit is unexpectedly None. Even if upstream logic/validation should guarantee presence, using .expect(\"annual activity limit must be computed for FullCost candidate pricing\") (or restructuring to avoid an Option once FullCost is selected) will produce a much more actionable failure mode.

Suggested change
asset.get_annual_fixed_costs_per_flow(annual_activity_limit.unwrap())
asset.get_annual_fixed_costs_per_flow(
annual_activity_limit.expect(
"annual activity limit must be computed for FullCost candidate pricing",
),
)

Copilot uses AI. Check for mistakes.
@tsmbland tsmbland merged commit 8a89d22 into main Mar 24, 2026
11 of 12 checks passed
@tsmbland tsmbland deleted the prices_tidy branch March 24, 2026 16:17
@github-project-automation github-project-automation bot moved this from 👀 In review to ✅ Done in MUSE Mar 24, 2026
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

Status: ✅ Done

Development

Successfully merging this pull request may close these issues.

3 participants