Skip to content

Improve Tulipa performance when running in a loop#1635

Open
datejada wants to merge 4 commits into
mainfrom
test-improvements
Open

Improve Tulipa performance when running in a loop#1635
datejada wants to merge 4 commits into
mainfrom
test-improvements

Conversation

@datejada

@datejada datejada commented Jun 29, 2026

Copy link
Copy Markdown
Member

This PR introduces several changes to improve performance in Tulipa, especially when running in a loop. Here is a summary:

  • Timer reset for run_scenario and run_rolling_horizon.
  • Profile aggregation now uses views instead of slices.
  • Storage constraints now use SQL window-computed previous_id / cycle_id instead of per-row DuckDB lookups and row.id - 1.
  • Seasonal storage inter-period inflow profile prep is now one grouped SQL query.
  • Reused inner-loop aggregation dictionaries in model/expression builders.
  • Removed only(collect(...)) in get_model_parameters.

Changes done with the help of CODEX:

Co-authored-by: Codex <noreply@openai.com>

Related issues

Closes #1634

Checklist

  • I am following the contributing guidelines
  • Tests are passing
  • Lint workflow is passing
  • Docs were updated and workflow is passing

@github-actions

github-actions Bot commented Jun 29, 2026

Copy link
Copy Markdown
Contributor

🤖 CompareMPS report

✅ MPS files match

@datejada datejada added benchmark PR only - Run benchmark on PR pr-test-all Add to an open pull request to make the tests on TestOnPRs.yml run on all OSs labels Jun 29, 2026
@github-actions

github-actions Bot commented Jun 29, 2026

Copy link
Copy Markdown
Contributor

Benchmark Results

92f72cf... 3ca8dab... 92f72cf... / 3ca8dab...
higher_level/EU/create_model 43.9 ± 1 s 40.6 ± 0.77 s 1.08 ± 0.033
higher_level/EU/input_and_constructor 10.8 ± 0.32 s 10.6 ± 0.3 s 1.01 ± 0.042
lower_level/EU/constraints 0.544 ± 0.0061 s 0.538 ± 0.0062 s 1.01 ± 0.016
lower_level/EU/create_internal_tables 9.48 ± 0.63 s 9.4 ± 0.3 s 1.01 ± 0.074
lower_level/EU/create_model 45.5 ± 0.63 s 40.7 ± 0.49 s 1.12 ± 0.02
lower_level/EU/profiles 0.391 ± 0.014 s 0.257 ± 0.015 s 1.52 ± 0.11
lower_level/EU/variables 0.479 ± 0.011 s 0.484 ± 0.0044 s 0.989 ± 0.025
time_to_load 2.34 ± 0.013 s 2.35 ± 0.018 s 0.999 ± 0.0096
92f72cf... 3ca8dab... 92f72cf... / 3ca8dab...
higher_level/EU/create_model 0.244 G allocs: 13.9 GB 0.236 G allocs: 13 GB 1.07
higher_level/EU/input_and_constructor 0.0413 G allocs: 1.59 GB 0.0412 G allocs: 1.58 GB 1
lower_level/EU/constraints 4.04 k allocs: 0.187 MB 4.04 k allocs: 0.187 MB 1
lower_level/EU/create_internal_tables 0.0409 G allocs: 1.56 GB 0.0409 G allocs: 1.56 GB 1
lower_level/EU/create_model 0.244 G allocs: 13.9 GB 0.236 G allocs: 13 GB 1.07
lower_level/EU/profiles 0.415 M allocs: 26 MB 0.328 M allocs: 23.3 MB 1.12
lower_level/EU/variables 0.764 k allocs: 0.0371 MB 0.764 k allocs: 0.0371 MB 1
time_to_load 0.145 k allocs: 11 kB 0.145 k allocs: 11 kB 1

Benchmark Plots

A plot of the benchmark results have been uploaded as an artifact to the workflow run for this PR.
Go to "Actions"->"Benchmark a pull request"->[the most recent run]->"Artifacts" (at the bottom).

@github-actions

Copy link
Copy Markdown
Contributor

📝 Check the documentation preview: https://tulipaenergy.github.io/TulipaEnergyModel.jl/previews/PR1635

@codecov

codecov Bot commented Jun 29, 2026

Copy link
Copy Markdown

Codecov Report

✅ All modified and coverable lines are covered by tests.
✅ Project coverage is 98.61%. Comparing base (92f72cf) to head (3ca8dab).

Additional details and impacted files
@@           Coverage Diff           @@
##             main    #1635   +/-   ##
=======================================
  Coverage   98.61%   98.61%           
=======================================
  Files          56       56           
  Lines        1874     1877    +3     
=======================================
+ Hits         1848     1851    +3     
  Misses         26       26           

☔ View full report in Codecov by Harness.
📢 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.

@datejada datejada marked this pull request as ready for review June 29, 2026 13:51
@datejada

Copy link
Copy Markdown
Member Author

Extra BenchmarkTools check, minimum of 3 samples, fresh connection per sample, baseline from detached HEAD worktree:

Case Phase Before ms After ms Before MiB After MiB Before allocs After allocs
Tiny EnergyProblem 7417.308 405.962 1.179 1.179 28401 28425
Tiny create_model! 4727.813 489.156 4.942 4.762 93909 92109
Storage EnergyProblem 2170.441 273.232 1.419 1.420 34803 34827
Storage create_model! 1725.075 490.894 9.441 8.914 178740 173394
Norse EnergyProblem 943.347 285.182 9.392 9.341 234540 232933
Norse create_model! 2209.009 663.451 88.049 79.720 1475587 1395424

@datejada datejada requested a review from abelsiqueira June 29, 2026 13:56
@datejada

Copy link
Copy Markdown
Member Author

@abelsiqueira, this PR implements improvements to speed up Tulipa's performance, especially when it's used repeatedly in a loop. I did the diagnosis and implementation with the help of CODEX. I decided to implement everything in one PR (but introduced by commit), since they all aim to achieve the same goal. I recognise that some of them are simple and might not have a major benefit. I think the most significant commits are the last two. For me, the refactor of id-1 is relevant, since using the LAG function in SQL makes sense to be faster and less time-consuming (I like that approach), and in the last commit you will see that the refactor also pre-allocates the workspace for the profiles and a refactor due to an old TO-DO (Creating inter_period profiles of inflows using the inflows profiles). All that helps to improve, as the benchmarks show. I ran the benchmark locally with the other cases to assess the benefits, as noted in the previous comment.

Anyway, before merging, it would be nice if you could have a look to see if it makes sense or if you have another suggestion.

Thanks!

Diego

@abelsiqueira abelsiqueira left a comment

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

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

Thanks @datejada, these look great, especially the profile changes. I've made non-blocking comments, so it's approved.
One thing, though, these extra benchmarks look really suspicious. Maybe they include precompilation time? Otherwise, it's over 10 times faster?!
If they are indeed correct, them an "easy" win would be to add Tiny, Storage, and Norse to benchmarks, and let the our normal benchmark script capture that. Should be more or less straightforward for the agent to create a new "level", like higher_level/Tiny/create_model. Either way, not a requirement for this PR because the improvements are clear

LAST_VALUE(cons.id) OVER (
PARTITION BY $id_partition_columns
ORDER BY $id_order_column
ROWS BETWEEN UNBOUNDED PRECEDING AND UNBOUNDED FOLLOWING

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

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

I think it's useful to explain what this does, but the full explanation is not really short.

From Claude, what I got is that this expands the window frame to all the rows, otherwise it would consider up to the current row. There is an alternative using descending ordering, but I don't think it's clearer. Also, if we assume that id is monotonically increase, we could use MAX, which would be easier.
(I feel like we do assume that, but I don't remember if we enforce it, so I'll continue as if we didn't).

Maybe something like

Suggested change
ROWS BETWEEN UNBOUNDED PRECEDING AND UNBOUNDED FOLLOWING
ROWS BETWEEN UNBOUNDED PRECEDING AND UNBOUNDED FOLLOWING -- frame expanding window to all rows, to avoid assuming ordering of ids

Comment thread src/model-preparation.jl
cte_storage_profiles AS (
SELECT DISTINCT
assets_profiles.profile_name,
assets_profiles.commission_year AS milestone_year

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

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

I don't remember our assumptions anymore, but this is one of the places (the only?) where commission_year = milestone_year. I don't remember why, but it's useful to mark this as a explicit choice for the future.

Suggested change
assets_profiles.commission_year AS milestone_year
assets_profiles.commission_year AS milestone_year -- here commission_year = milestone_year

Comment thread src/model-preparation.jl
Comment on lines +857 to +858
inter_period[(row.profile_name, row.milestone_year, row.scenario)] =
convert(Vector{Float64}, row.values)

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

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

It's not clear to me why this doesn't need the if length(values) > 0 as it did before, but I also don't remember why we needed it, so I guess it's fine

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

benchmark PR only - Run benchmark on PR pr-test-all Add to an open pull request to make the tests on TestOnPRs.yml run on all OSs

Projects

None yet

Development

Successfully merging this pull request may close these issues.

Slow performance when running Tulipa in a loop

2 participants