Skip to content

Conversation

@tsmbland
Copy link
Collaborator

@tsmbland tsmbland commented Jan 23, 2026

Description

Since #1020 we have a file allowing users to specify process investment constraints, but these were so far not being used.

In this PR, I've added a function calculate_investment_limits_for_candidates which use values from this input file to cap the amount of investment allowed for each process. These limits are calculated per-agent based on the agent's share of the commodity demand and the number of years elapsed since the previous milestone year (see discussion in #124).

The implementation was ultimately quite easy because we already have a remaining_candidate_capacity map to cap investments (currently based on demand limiting capacity), so we just needed to modify this map based on the provided investment limits (if any). An alternative would have been to modify the capacity field of candidate assets, but I decided against this as this would modify the tranching behaviour which I didn't want.

Related things left to do:

  • implement growth limits and total capacity limits (the overall behaviour should be similar to MUSE1)
  • express limits as a range rather than just an upper limit as we currently do - in this case we'd also need to implement a lower bound on investments and think about how to do this

Fixes #1069

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

@codecov
Copy link

codecov bot commented Jan 23, 2026

Codecov Report

❌ Patch coverage is 87.00000% with 13 lines in your changes missing coverage. Please review.
✅ Project coverage is 87.50%. Comparing base (2ab3ed7) to head (25304ed).
⚠️ Report is 14 commits behind head on main.

Files with missing lines Patch % Lines
src/simulation/optimisation.rs 42.10% 8 Missing and 3 partials ⚠️
src/simulation/investment.rs 95.12% 1 Missing and 1 partial ⚠️
Additional details and impacted files
@@            Coverage Diff             @@
##             main    #1096      +/-   ##
==========================================
+ Coverage   87.42%   87.50%   +0.08%     
==========================================
  Files          55       55              
  Lines        7379     7507     +128     
  Branches     7379     7507     +128     
==========================================
+ Hits         6451     6569     +118     
- Misses        631      637       +6     
- Partials      297      301       +4     

☔ 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 marked this pull request as ready for review January 26, 2026 11:39
Copilot AI review requested due to automatic review settings January 26, 2026 11:39
Copy link
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

This PR implements investment limits for processes using the process_investment_constraints.csv input file that was introduced in PR #1020. Investment limits cap the amount each agent can invest in a process based on annual addition limits, scaled by the agent's share of commodity demand and the number of years elapsed since the previous milestone year.

Changes:

  • Added calculate_investment_limits_for_candidates function to compute per-agent investment limits
  • Changed addition_limit field type from f64 to Capacity for better type safety
  • Added get_annual_addition_limit method to support future extension with growth and total capacity limits

Reviewed changes

Copilot reviewed 5 out of 5 changed files in this pull request and generated no comments.

Show a summary per file
File Description
src/simulation/investment.rs Implements the core logic for calculating and applying investment limits to candidate assets, integrates limits into the asset selection process, and adds comprehensive tests
src/process.rs Changes addition_limit type to Capacity and adds get_annual_addition_limit method for future extensibility
src/input/process/investment_constraints.rs Updates type handling throughout to use Capacity instead of f64 and updates all tests accordingly
examples/two_regions/process_investment_constraints.csv Adds example investment constraint data for the two_regions example
examples/muse1_default/process_investment_constraints.csv Adds example investment constraint data for the muse1_default example

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

@tsmbland tsmbland requested review from Aurashk and alexdewar January 26, 2026 11:43
@tsmbland
Copy link
Collaborator Author

Also, forgot to mention in the description, but I've added investment limits for the muse1_default and two_regions models in line with MUSE1. These limits aren't actually restrictive enough to do anything, but you can play around with them to see it working in action.

And one other thing I've just realised is that these limits won't necessarily be complied with in the circularities algorithm, since we allow capacities to change after the (now constrained) investment stage within capacity_margin, which may exceed the process investment limit, so I need to think about what to do here...

@tsmbland
Copy link
Collaborator Author

Converting back to draft - some things I want to tidy up

@tsmbland tsmbland marked this pull request as draft January 26, 2026 13:44
@tsmbland
Copy link
Collaborator Author

@alexdewar This is getting a bit fiddly. Since investment limits depend on an agent property (namely the agent's commodity_portion), it gets a bit messy calculating the limits as you have to pass around the map of agents then go through matching each asset to the agents map. It's fine in select_assets_for_single_market since we're looking at agents one by one and already retrieve the commodity_portion, but for the cycle balancing in select_assets_for_cycle we're lumping assets from different agents together, and everything I've tried just got really icky.

It would be so much easier if assets stored the Agent (or Rc<Agent> like we do for processes), because then for assets with an assigned agent you could just do asset.max_installable_capacity() without having to pass anything else around. Seems like this would be too big a refactor to do in this PR, but do you think that's a good route to go down?

@alexdewar
Copy link
Collaborator

It would be so much easier if assets stored the Agent (or Rc<Agent> like we do for processes), because then for assets with an assigned agent you could just do asset.max_installable_capacity() without having to pass anything else around. Seems like this would be too big a refactor to do in this PR, but do you think that's a good route to go down?

Yes I do! There have been a couple of times where I started doing that for other reasons, but ended up not needing it, so never opened a PR.

I haven't looked at this PR yet, but we have a couple of options. We could merge as is (possibly adding a warning) or leave this branch here and I can fix it up once we've done the other refactor. Let's talk about it in the meeting.

@tsmbland
Copy link
Collaborator Author

tsmbland commented Jan 27, 2026

@alexdewar Sounds good.

I've added the constraints for the circularities algorithm in b2eb10d as I wanted this to be complete, albeit a bit messy.

If we go down the route of what we've suggested above, then we could just retrieve the investment limit directly for each asset in add_capacity_variables, rather than having to pre-assemble a map and pass it around.

But, with that in mind, I think this is ready to review

@tsmbland tsmbland marked this pull request as ready for review January 27, 2026 10:50
Copilot AI review requested due to automatic review settings January 27, 2026 10:50
Copy link
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 7 out of 7 changed files in this pull request and generated 6 comments.


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

let upper = (1.0 + capacity_margin) * units as f64;
let mut upper = (1.0 + capacity_margin) * units as f64;
if let Some(limit) = capacity_limit {
upper = upper.min(limit.n_units().unwrap() as f64);
Copy link

Copilot AI Jan 27, 2026

Choose a reason for hiding this comment

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

The use of unwrap() here is safe due to the type compatibility check on lines 676-685, but it would be more robust to use expect() with a descriptive message. This would make debugging easier if the assertion logic somehow changes in the future or if there's an unexpected edge case. Consider changing to: upper = upper.min(limit.n_units().expect("Capacity limit should be discrete here due to compatibility check") as f64);

Suggested change
upper = upper.min(limit.n_units().unwrap() as f64);
upper = upper.min(
limit
.n_units()
.expect("Capacity limit should be discrete here due to compatibility check")
as f64,
);

Copilot uses AI. Check for mistakes.
Comment on lines +699 to +719
fn calculate_investment_limits_for_candidates(
opt_assets: &[AssetRef],
commodity_portion: Dimensionless,
) -> HashMap<AssetRef, AssetCapacity> {
// Calculate limits for each candidate asset
opt_assets
.iter()
.filter(|asset| !asset.is_commissioned())
.map(|asset| {
// Start off with the demand-limiting capacity (pre-calculated when creating candidate)
let mut cap = asset.capacity();

// Cap by the addition limits of the process, if specified
if let Some(limit_capacity) = asset.max_installable_capacity(commodity_portion) {
cap = cap.min(limit_capacity);
}

(asset.clone(), cap)
})
.collect()
}
Copy link

Copilot AI Jan 27, 2026

Choose a reason for hiding this comment

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

The new function calculate_investment_limits_for_candidates lacks test coverage. Given that this is a core function implementing the investment limits feature (as mentioned in the PR description), it would be valuable to add unit tests to verify the logic, particularly:

  1. That it correctly filters only uncommissioned assets
  2. That it properly combines demand-limiting capacity with investment constraint limits
  3. That the minimum of the two limits is used when both are present

Copilot uses AI. Check for mistakes.
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

Implement process investment constraints

3 participants