Skip to content

Fix get_demand_limiting_capacity to account for seasonal/annual activity limits#1326

Draft
tsmbland wants to merge 4 commits into
mainfrom
demand_limiting_capacity_fix
Draft

Fix get_demand_limiting_capacity to account for seasonal/annual activity limits#1326
tsmbland wants to merge 4 commits into
mainfrom
demand_limiting_capacity_fix

Conversation

@tsmbland

@tsmbland tsmbland commented Jun 3, 2026

Copy link
Copy Markdown
Collaborator

Description

Part of the problem were facing in #1319 is due to the fact that we're incorrectly calculating demand limiting capacities for processes with seasonal/annual availability limits, which prevents installing enough capacity to meet demand.

For example, wind turbines in the muse1_default model have an annual availability limit of 0.4, which currently isn't taken into account when calculating DLC

Since different processes can have availability limits defined at different levels, which isn't known at the point of calculating DLC, the easiest approach I could think of was to calculate DLC at every possible level (timeslice/season/annual) and take the max.

Ultimately I'm hoping that if we go ahead with plans to change the way tranching works then we may not need this function any more, which is partly why I haven't written a proper docstring or any tests (that, and laziness, and that copilot which I'd usually use to write these tests is now prohibitively expensive)

To be honest I'm still not sure this correctly handles timeslices with zero activity. Kinda hoping we can get rid of this function!

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

@codecov

codecov Bot commented Jun 3, 2026

Copy link
Copy Markdown

Codecov Report

❌ Patch coverage is 95.55556% with 2 lines in your changes missing coverage. Please review.
✅ Project coverage is 89.54%. Comparing base (4044358) to head (afcc9b6).

Files with missing lines Patch % Lines
src/time_slice.rs 83.33% 2 Missing ⚠️
Additional details and impacted files
@@            Coverage Diff             @@
##             main    #1326      +/-   ##
==========================================
+ Coverage   89.51%   89.54%   +0.03%     
==========================================
  Files          58       58              
  Lines        8537     8572      +35     
  Branches     8537     8572      +35     
==========================================
+ Hits         7642     7676      +34     
- Misses        580      581       +1     
  Partials      315      315              

☔ 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 commented Jun 4, 2026

Copy link
Copy Markdown
Collaborator Author

Would have to change if we go ahead with #1329

Something like

/// Get the maximum required capacity across time slices.
fn get_demand_limiting_capacity(
    time_slice_info: &TimeSliceInfo,
    asset: &Asset,
    commodity: &Commodity,
    demand: &DemandMap,
) -> Capacity {
    let coeff = asset.get_flow(&commodity.id).unwrap().coeff;
    let mut capacity = Capacity(0.0);
    let mut demand_cache: HashMap<_, Flow> = HashMap::new();

    // Calculate demand-limiting capacity at each timeslice level and take the max.
    // This is necessary because process availability limits at the seasonal/annual level may
    // necessitate higher capacity than activity limits at finer timeslice levels.
    for level in TimeSliceLevel::iter() {
        for selection in time_slice_info.iter_selections_at_level(level) {
            // Maximum supply within this selection according to the asset's activity limits.
            let max_supply_for_selection = *asset
                .get_activity_per_capacity_limits_for_selection(&selection)
                .end()
                * coeff;

            // Selections with zero supply would imply infinite demand-limiting capacity,
            // so they do not contribute to the maximum.
            if max_supply_for_selection == FlowPerCapacity(0.0) {
                continue;
            }

            // Serviceable demand within this selection.
            //
            // Demand is stored at the commodity balance level. Demand from a balance
            // bucket contributes if:
            //   1. The bucket is contained within this selection, and
            //   2. The asset can operate in at least one constituent timeslice
            //      within that bucket.
            //
            // This reflects the fact that demand is fungible within a balance
            // bucket but not across balance buckets.
            let serviceable_demand_for_selection = *demand_cache
                .entry(selection.clone())
                .or_insert_with(|| {
                    selection
                        .iter_at_level(time_slice_info, commodity.time_slice_level)
                        .unwrap()
                        .filter(|(bucket, _)| {
                            bucket.iter(time_slice_info).any(|(time_slice, _)| {
                                *asset
                                    .get_activity_per_capacity_limits(time_slice)
                                    .end()
                                    > ActivityPerCapacity(0.0)
                            })
                        })
                        .map(|(bucket, _)| demand[&bucket])
                        .sum()
                });

            // Calculate demand-limiting capacity for this selection and take the
            // maximum across all selections.
            capacity = capacity.max(
                serviceable_demand_for_selection / max_supply_for_selection,
            );
        }
    }

    capacity
}

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.

1 participant