Skip to content

Use parent assets rather than children for dispatch#1124

Merged
alexdewar merged 4 commits intomainfrom
parent-assets-for-dispatch
Feb 13, 2026
Merged

Use parent assets rather than children for dispatch#1124
alexdewar merged 4 commits intomainfrom
parent-assets-for-dispatch

Conversation

@alexdewar
Copy link
Collaborator

Description

For divisible assets, we currently include every child asset separately in the dispatch optimisation, but now that divided assets all have parent assets, we can use these parent assets instead. This means that we assume all the flows will be the same for each child asset, which seems reasonable, although note that previously HiGHS would settle on solutions where they would be different.

The implementation I've come up with is a bit horrible, frankly, but I'm hoping that we can improve on it as we incrementally change the rest of the code to work with parent assets rather than children. I've left the external API for dispatch the same -- you still input child assets and get child assets out the other side -- but, internally, it converts the child assets to parents before running dispatch. Once child assets have been yeeted (#1123), we can remove all this converting back and forth.

I benchmarked a model with 10,000 child assets and there was an ~8% speedup. We'll likely get an even better speedup with #1045, but it's not bad.

Closes #1043.

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

Copilot AI review requested due to automatic review settings February 11, 2026 16:30
@codecov
Copy link

codecov bot commented Feb 11, 2026

Codecov Report

✅ All modified and coverable lines are covered by tests.
✅ Project coverage is 87.52%. Comparing base (bb99332) to head (f96b5ff).
⚠️ Report is 16 commits behind head on main.

Additional details and impacted files
@@            Coverage Diff             @@
##             main    #1124      +/-   ##
==========================================
- Coverage   87.55%   87.52%   -0.03%     
==========================================
  Files          55       55              
  Lines        7580     7626      +46     
  Branches     7580     7626      +46     
==========================================
+ Hits         6637     6675      +38     
- Misses        640      649       +9     
+ Partials      303      302       -1     

☔ 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.

@alexdewar alexdewar requested a review from Aurashk February 11, 2026 16:38
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.

Copilot encountered an error and was unable to review this pull request. You can try again by re-requesting a review.

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 3 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 +674 to +677
solution.flow_map.set(Some(create_flow_map(
self.existing_assets,
&self.model.time_slice_info,
solution.iter_activity(),
Copy link

Copilot AI Feb 13, 2026

Choose a reason for hiding this comment

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

create_flow_map(...) is populated from solution.iter_activity(), which includes candidate assets when dispatch is run with candidates. That means the stored FlowMap can include assets without an AssetID (e.g., Candidate), contradicting the method docs (“existing assets”) and risking panics if it’s ever passed to DataWriter::write_flows() (which does asset.id().unwrap()). Consider restricting the activity iterator to existing assets only (or filtering out non-commissioned assets) when building the flow map.

Suggested change
solution.flow_map.set(Some(create_flow_map(
self.existing_assets,
&self.model.time_slice_info,
solution.iter_activity(),
// Only include activities for existing assets in the stored flow map.
let existing_assets = self.existing_assets;
let activity_iter = solution
.iter_activity()
.filter(|(asset, ..)| existing_assets.contains(asset));
solution.flow_map.set(Some(create_flow_map(
self.existing_assets,
&self.model.time_slice_info,
activity_iter,

Copilot uses AI. Check for mistakes.
Copy link
Collaborator Author

Choose a reason for hiding this comment

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

Ha, this doc comment has been wrong for ages, but Copilot is the first to notice.

Comment on lines +208 to +214
for commodity_id in asset.iter_flows().map(|flow| &flow.commodity.id) {
for time_slice in time_slice_info.iter_ids() {
let flow = flows[&(parent.clone(), commodity_id.clone(), time_slice.clone())];
flows.insert(
(asset.clone(), commodity_id.clone(), time_slice.clone()),
flow,
);
Copy link

Copilot AI Feb 13, 2026

Choose a reason for hiding this comment

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

In the child-flow copy loop, flows[&(parent.clone(), commodity_id.clone(), time_slice.clone())] will panic if the parent/time-slice/commodity key is missing. If assumptions change (e.g., differing flow sets between parent/child, or future filtering), this will become a hard-to-diagnose crash. Consider using get(...).expect(...) with a clearer message, or handling the missing-key case explicitly.

Copilot uses AI. Check for mistakes.
Copy link
Collaborator

@Aurashk Aurashk left a comment

Choose a reason for hiding this comment

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

Looks ok to me. Just thinking out loud but maybe in future we want to put an intermediate data structure here which extracts whatever we need from the AssetRefs e.g activity coefficient , (asset, time) key for variable map. Then we feed the new struct to new_with_activity_vars. Then it might be easier to separate what the optimiser needs to know about the assets from the data stored about assets in general.

///
/// Child assets are converted to their parents and non-divisible assets are returned as is. Each
/// parent asset is returned only once.
fn convert_assets_to_parents(assets: &[AssetRef]) -> impl Iterator<Item = AssetRef> {
Copy link
Collaborator

Choose a reason for hiding this comment

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

maybe change the name of this to divide_assets or something, to make clearer that it's retaining all the non divisible ones. Or even prepare_assets_for_dispatch, because it's essentially converting to a format the dispatch API will understand

Copy link
Collaborator Author

Choose a reason for hiding this comment

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

Good point. I went with get_parent_or_self in the end.

@alexdewar
Copy link
Collaborator Author

Looks ok to me. Just thinking out loud but maybe in future we want to put an intermediate data structure here which extracts whatever we need from the AssetRefs e.g activity coefficient , (asset, time) key for variable map. Then we feed the new struct to new_with_activity_vars. Then it might be easier to separate what the optimiser needs to know about the assets from the data stored about assets in general.

If I've understood you correctly, I think the problem with this is that we need the actual AssetRef structs for the dispatch code anyway, because we use them as keys for the variable map anyway, so whatever data structure we pass in, we'd still need the assets themselves. Am I missing something?

@alexdewar alexdewar merged commit cabdbf1 into main Feb 13, 2026
19 of 22 checks passed
@alexdewar alexdewar deleted the parent-assets-for-dispatch branch February 13, 2026 13:55
@alexdewar
Copy link
Collaborator Author

Oh crap, I forgot to push my changes before clicking merge 🙈.

@alexdewar alexdewar mentioned this pull request Feb 13, 2026
11 tasks
@Aurashk
Copy link
Collaborator

Aurashk commented Feb 13, 2026

Looks ok to me. Just thinking out loud but maybe in future we want to put an intermediate data structure here which extracts whatever we need from the AssetRefs e.g activity coefficient , (asset, time) key for variable map. Then we feed the new struct to new_with_activity_vars. Then it might be easier to separate what the optimiser needs to know about the assets from the data stored about assets in general.

If I've understood you correctly, I think the problem with this is that we need the actual AssetRef structs for the dispatch code anyway, because we use them as keys for the variable map anyway, so whatever data structure we pass in, we'd still need the assets themselves. Am I missing something?

That's a good point, I think if you wanted to do it you'd update these maps so they use the new struct (say AssetDispatchVariables) instead of AssetRef as keys e.g.
type ActivityVariableMap = IndexMap<(AssetDispatchVariables, TimeSliceID), Variable>
then you would want to store a map of AssetRef -> AssetDispatchVariables (probably could be unordered) in VariableMap so that you could recover dispatch details from each asset ref through the methods in VariableMap.

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.

Dispatch should be run over asset groups, not each individual unit

2 participants

Comments