Skip to content

Feat/highs speedups - #898

Open
sjpfenninger wants to merge 2 commits into
mainfrom
feat/highs-speedups
Open

Feat/highs speedups#898
sjpfenninger wants to merge 2 commits into
mainfrom
feat/highs-speedups

Conversation

@sjpfenninger

Copy link
Copy Markdown
Member

Fixes #

Summary of changes in this pull request

  • Shadow price extraction in the HiGHS backend no longer iterates over all constraint elements. This was causing severe slowdowns, making the HiGHS backend unusable for large models with shadow price extraction.
  • On top of that, this branch also batches variable and constraint construction as an extra optimisation. The HiGHS backend is still slightly slower than the Gurobi one, and the slowdown worsens the larger the model. For the national-scale built-in model (full time series) it's about 5% slower.

Reviewer checklist

  • Test(s) added to cover contribution
  • Documentation updated
  • Changelog updated
  • Coverage maintained or improved

sjpfenninger and others added 2 commits September 8, 2026 15:28
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
@codecov

codecov Bot commented Sep 9, 2026

Copy link
Copy Markdown

Codecov Report

❌ Patch coverage is 86.66667% with 8 lines in your changes missing coverage. Please review.
✅ Project coverage is 97.38%. Comparing base (c2c0549) to head (05dcd63).

Files with missing lines Patch % Lines
src/calliope/backend/highs_backend_model.py 86.66% 5 Missing and 3 partials ⚠️
Additional details and impacted files
@@            Coverage Diff             @@
##             main     #898      +/-   ##
==========================================
- Coverage   97.52%   97.38%   -0.14%     
==========================================
  Files          39       39              
  Lines        5045     5092      +47     
  Branches      658      664       +6     
==========================================
+ Hits         4920     4959      +39     
- Misses         65       70       +5     
- Partials       60       63       +3     
Files with missing lines Coverage Δ
src/calliope/backend/highs_backend_model.py 97.67% <86.66%> (-2.33%) ⬇️
🚀 New features to boost your workflow:
  • ❄️ Test Analytics: Detect flaky tests, report on failures, and find test suite problems.

@irm-codebase irm-codebase 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.

Looks good. Had to do some brain churning to understand the method.
Overall it looks good. I trust our tests catch poor implementation thanks to our static result checkers.

Requests:

  • Modify comments and docstrings to get rid of chaff
  • Make sure test coverage is complete.

Comment on lines +93 to +95
Mirrors the broadcasting done by `_apply_func`, so that components added to
HiGHS in batches end up in arrays with the same shape, dims and coords as an
element-wise apply would produce.

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.

Two things:

  • Wouldn't other backends benefit from this approach, if it is more efficient?
    If it can be demonstrated (e.g., via memray, we should consider moving this to the parent class.
  • Let us avoid referencing other functions in comments (_apply_func). Names change and this is bound to become obsolete.

Comment on lines +115 to +118
def _scatter_objects(
template: xr.DataArray, mask: np.ndarray, objs: list
) -> xr.DataArray:
"""Place backend objects at the `mask` positions of an otherwise-NaN array.

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.

Another generic function that perhaps move upwards if other backends would benefit from it.

Comment on lines +150 to +158
status = self._instance.addCols(
n_new,
np.zeros(n_new),
np.asarray(lb_vals[mask], dtype=float),
np.asarray(ub_vals[mask], dtype=float),
0,
np.zeros(n_new, dtype=np.int32),
np.empty(0, dtype=np.int32),
np.empty(0),

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.

Trying to summarise this to see if I got the behaviour right.

  • total new variables to add
  • objective coefficient (0).
  • lower bound
  • upper bound
  • coefficient in constraints (0)
  • constraint coefficient rows indices (none)
  • coefficient values (none).

Basically, this puts the variables in the model, does not initialise or set coefficients for them in the objective or the constraint matrix. Kind of 'non-parametric' variable initialisation.

Comment on lines +146 to +147
# Variables are added in one batch: `addVariable` allocates arrays and queries
# the model on every call, which dominates build time on large models.

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.

Do not refer to code in comments (addVariable) to avoid confusion in the future.
It's fine to just state that this is more efficient in HiGHS, and state what this actually does (create non-parametric variables in the model).

summed, mirroring `highs_linear_expression.unique_elements`. HiGHS rejects a
row that references the same column twice, so the merge is required, not an
optimisation.

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.

Please adapt AI over explained text into actual docstrings.

Suggested change
Entries are sorted by (row, column) and duplicate columns within a row are summed (3x + 3x + 3y = 6x + y).
Otherwise, HiGHS would reject a row that references the same column twice.

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.

Github suggestions are so, so broken nowadays :(

Comment on lines +256 to +257
# E.g. a coefficient whose absolute value is below `small_matrix_value`,
# which HiGHS drops, flagging it with a warning status.

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.

remove. Self evident.

lengths = np.empty(n_new, dtype=np.int64)
idxs: list[list[int]] = []
vals: list[list[float]] = []
for idx, expr in enumerate(exprs):

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.

Another attempt of trying to summarise what this is doing.

This is the row equivalent of _add_variable above (which uses addCols).

  • fetches the bonds from the expression and builds its compressed sparse row matrix equivalent (to fit HiGHS). This is a 'squashed' version of the matrix constructed by the model, built for higher efficiency. Note that if the problem is very 'dense' this will be less efficient than regular notation. In most applications sparse will win, though.
    • starts: where each constraint 'begins'
    • cols: which variables are referenced ($3x_1 + 2x_3$ -> [1, 3])
    • coefs: the coefficients ($3x_1 + 2x_3$ -> [3, 2])
  • get the current number of rows (constraints) already in the model to make sure we do not forget / overwrite them.
  • Add the set of new constraints in one go via addRows
    • n_new: number of constraints to add
    • lower / upper: constraint bounds
    • coefs.size: total number of coefficients being added.
    • starts: where each constraint starts
    • cols: variable index for the coefficient in each constraint
    • coefs: value of each coefficient
  • Check if all is well, blow up otherwise.
  • Return the location of the new constraints in the backend so we can extract results later.

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.

2 participants