New tranching approach#1357
Conversation
|
Failing for |
|
I've had a look at the failing models, and these are all due to commodities having zero/questionable shadow prices in one year, which makes activity coefficients negative the following year:
These all seem like fairly normal scenarios so makes me less convinced that #1347 should be a special feature hidden behind a warning, unless there's a deeper issue here that we can fix. In the latter two cases, it invests in assets to meet demand in the other seasons, but then exits when it has leftover demand in summer that it's chosen not to meet with these assets. I think an issue with the approach suggested in #1347 is that it would allow the system to invest in extra capacity to meet the demand in summer, whereas really it would have been better to utilise the existing assets in summer all along. |
|
@ahawkes I've had a go at implementing the new capacity selection approach as described in #1358. Two questions/discussion points:
|
Not sure about 10/capacity_to_activity. How about enough capacity to meet about 1/10 of the capacity requirements of the original demand profile - but instead of the DLC approach which focused on the worst time slice (and got capacities that are too large) - we focus on the best (highest availability) time slice - and choose a capacity that can serve 1/10 of the capacity needs in that time slice. There are probably pitfalls to this approach - feel free to refine.
For this issue, remember we were adding epsilon to make sure things dispatch for the old NPV formulation? I guess this is not similar to that? If yes, then maybe I could take a look at one that is failing to see what's going on? |
Ok... that would take us closer to where we were before. The key difference is whether capacities are defined upfront for all years (i.e. in the input data) or whether they depend on run-time info. I assumed you wanted the former so that's how I've done it. In any case, I'd probably lean into one direction or the other, rather than a mixed approach.
That helps when activity coefficients are zero. In this case, coefficients are negative, so this is a different problem. |
Not sure about this. What if demand is zero (or extremely low) in timeslices with high availability (but may be higher elsewhere). Capacities could end up extremely small, which could mean a huge number of appraisal rounds if there aren't other more feasible technologies. Feels like there could be equally many pitfalls with this approach compared to the DLC approach. |
Yes, fair enough. Though it seems there should be a way to choose based on availabilities, time sliced demands, etc - just needs to be more nuanced than DLC or this alternative demand-based approach. Anyway, in order to have something working, perhaps let's try with 1/cap2act as you suggest and see how it goes? Should we also make it a configurable parameter? If yes, would it be associated with process or commodity? I think I prefer the former. |
As it currently is in this PR:
I suggest we make the default |
4d2f352 to
9ed8b03
Compare
c7fb67c to
a85bd91
Compare
9ed8b03 to
10b4fd5
Compare
a85bd91 to
033146d
Compare
033146d to
8016b13
Compare
126cdbe to
d53c8c8
Compare
Codecov Report❌ Patch coverage is Additional details and impacted files@@ Coverage Diff @@
## main #1357 +/- ##
==========================================
- Coverage 89.68% 86.45% -3.24%
==========================================
Files 58 58
Lines 8406 8341 -65
Branches 8406 8341 -65
==========================================
- Hits 7539 7211 -328
- Misses 550 820 +270
+ Partials 317 310 -7 ☔ View full report in Codecov by Harness. 🚀 New features to boost your workflow:
|
Looks like you tell Copilot to ignore certain folders: https://docs.github.com/en/copilot/concepts/context/content-exclusion Maybe we should do that. Presumably reading all those files eats tokens too! |
alexdewar
left a comment
There was a problem hiding this comment.
Code looks good as always!
I've got a couple of questions, but happy to approve otherwise. I'm guessing the answer is "yes" to both, but I thought I should double-check!
- Is @ahawkes happy with this approach, at least for now? We definitely don't want the capacities to be dynamic instead?
- Do we have a relatively high degree of confidence we can unbreak the failing models? Obvs
circularity_npvis failing already, so nothing lost there. I tried dispatching the first non-feasible option for these models (seehack-select-nonfeasiblebranch) and they still failed.
As Copilot refused to review this, I used local Copilot to do a review and it spotted some things. I've pasted them below. Point 3 seems interesting... do we need to clamp capacity somewhere to avoid exceeding addition limits?
-
Release notes not updated (required by
AGENTS.md).docs/release_notes/upcoming.mdhas no
entry for this work, but it contains breaking input-format changes that users must act on:processes.csv:unit_sizecolumn replaced bycapacity_granularity+is_divisible.model.toml:capacity_limit_factorremoved;default_capacity_granularity_factoradded.
These belong under "Breaking changes" (and arguably "New features" for
is_divisible). -
Stale documentation in
docs/file_formats/input_files.md. The YAML schemas were updated, but
the markdown reference table was not:- Line 21 still documents the removed
capacity_limit_factor. - Line 234 still documents
unit_size(with the oldn = ceil(C / U)description) instead of
capacity_granularity/is_divisible.
(The
unit_sizereferences indocs/release_notes/v2.1.0.mdare historical and should be left
as-is.) - Line 21 still documents the removed
-
Addition limits are no longer strictly enforced — they round up to a multiple of
capacity_granularity. Inupdate_assets(src/simulation/investment.rs:807-822), a selected
candidate always contributes a fullcapacity_granularity, and the remaining-capacity check
happens after the increment:*remaining_capacity = *remaining_capacity - best_asset.capacity(); if remaining_capacity.total_capacity() <= Capacity(0.0) { /* remove */ }
For an indivisible process with
addition_limit = 250andcapacity_granularity = 100, the
candidate gets selected three times (remaining: 150 → 50 → −50), investing 300 total — exceeding
the limit by nearly a full granularity. Previouslymax_installable_capacity(amincap)
bounded this. If exceeding the configuredaddition_limitis not acceptable, the loop should
skip/clamp the final tranche when it would overshoot. Worth confirming this is intended.
| capacity_to_activity > ActivityPerCapacity(0.0), | ||
| "Error in process {}: capacity_to_activity must be > 0", | ||
| process_raw.id | ||
| ); |
There was a problem hiding this comment.
Perhaps this should be listed as a bug fix in the release notes?
| capacity_to_activity: Option<ActivityPerCapacity>, | ||
| unit_size: Option<Capacity>, | ||
| capacity_granularity: Option<Capacity>, | ||
| is_divisible: Option<bool>, |
There was a problem hiding this comment.
Alternatively, you could have it default to false:
#[serde(default)]
is_divisible: bool,| ); | ||
|
|
||
| Ok(()) | ||
| } |
There was a problem hiding this comment.
A lot of the checks look like this. I've wondered if we should write a macro for it.
| self.is_valid() && other.is_valid(), | ||
| self.metric.is_some() && other.metric.is_some(), | ||
| "Cannot compare non-valid outputs" | ||
| ); |
There was a problem hiding this comment.
Another way of doing this would be to unwrap the metrics together, e.g.:
let (metric1, metric2) = self.metric.as_ref().zip(other.metric.as_ref()).expect("Cannot compare non-valid outputs");Then you wouldn't need to unwrap() below.
| ) | ||
| .collect::<Vec<_>>(); | ||
| let opt_assets = get_asset_options(existing_assets, agent, commodity, region_id, year) | ||
| .collect::<Vec<_>>(); |
There was a problem hiding this comment.
Itertools lets you do this:
| .collect::<Vec<_>>(); | |
| .collect_vec(); |
| best_asset.make_mut().set_capacity(capacity); | ||
| // Otherwise add it to the list of best assets | ||
| best_assets.push(best_asset); | ||
| } |
There was a problem hiding this comment.
Not really related to this PR, but I've been wondering whether we should convert Candidate assets to Selected here rather than in select_best_assets. What do you think?
Description
Implements the approach suggested in #1358 for selecting tranche sizes, based on a new
capacity_granularityparameter inprocesses.csv. For a "sensible" default I've gone fordefault_capacity_granularity_factor / capacity_to_activity, wheredefault_capacity_granularity_factoris another new parameter defined inmodel.toml. I'm relying on this default in all the example models, rather than manually specifying.I've gone for
default_capacity_granularity_factor = 100, which to be honest seems a bit high (if cap2act=1 then the minimum capacity is 100), but this led to a roughly similar number of appraisal rounds compared to before. Making this much smaller would increase running time a lot (not such a problem for these small models), and lead to massive debug files, which is the main reason I've kept it at 100 for now (rather than 10 or 1).This interacts with the existing divisible assets implementation by making
unit_sizeequal tocapacity_granularityfor divisible assets (via a method). A newis_divisibleparameter controls whether processes are divisible or not. Not much else needed to change here.Still don't think this is really a complete solution. I quite like being able to toggle granularity vs performance, but I think I agree with Adam that capacities should in some way scale with demand, otherwise you could end up with a huge number of appraisal rounds when demands are high, or an inability to invest in small capacities when demand is low. But, for now, we don't have an agreed upon alternative.
Two regression tests are failing:
circularity_npv(which is failing on main) andcircularity, both of which are due to #1347, with zero shadow prices being the ultimate culprit (see #1357 (comment))Fixes #1358
Type of change
Key checklist
$ cargo test$ cargo docpresent in the previous release
Further checks