Hacktoberfest 2026: le issue che i maintainer hanno segnato per ottobre, aperte e adatte ai principianti. Sfoglia le issue Hacktoberfest

perf(solvers): name strings and getLp/passModel round trip dominate to_highspy (and gurobi) build time

Chiusa
#978 1 commento 0 reazioni 0 assegnatari Vedi su GitHub

I maintainer di solito rispondono entro 1 giorno

Nessuno ha ancora preso questa issue.

Valutazione

Difficoltà
4/5
Tempo stimato
3-5 giorni
Idoneità per principianti
48/100
Tipo di issue
Refactoring
Chiarezza
Abbastanza chiara
Stato di attività
Attiva
Stack tecnologico
numpy, python

Direzione di ricerca

Start in linopy/solvers.py at Highs._build_solver_model and in linopy/io.py at get_printers_scalar, to_highspy, and to_gurobipy. Run the supplied repro.py with uv run python repro.py to establish the naming and conversion timings. Done means reducing the name-related build overhead while preserving the expected solver model names and behavior for both direct APIs.

Scritto dal modello di indicizzazione a partire dal testo della issue.

Descrizione

performance solver interface

[!NOTE]
The following content was generated by AI.

Describe the feature you'd like to see

Once the sparse/CSR path makes building the matrices cheap, setting variable/constraint names is the largest single cost of the direct solver APIs. Found while working on #974 (sparse/CSR plan #972); measured on feat/csr-boundaries @ 0cf4a200.

Model with 200,000 vars, 218,000 cons and 596,000 nnz (frozen constraints), best of 5 runs:

step time
to_highspy(m) (names on, the default) 117–121 ms
to_highspy(m, set_names=False) 47–49 ms
of which print_variables + print_constraints (build Python list[str]) 28–29 ms
of which h.getLp() + assigning lp.col_names_ / lp.row_names_ (list to std::vector<string>) 13–14 ms
of which h.passModel(lp) round trip 11–12 ms
to_gurobipy(m) vs to_gurobipy(m, set_names=False) 566 ms vs 467 ms (+~100 ms)

So names make to_highspy about 2.4x slower. In Highs._build_solver_model (linopy/solvers.py) the names are added after the model is built: lp = h.getLp() copies the whole LP out, names are set, and h.passModel(lp) copies it back in. That round trip copies the matrix twice only to attach names. get_printers_scalar (linopy/io.py) already builds the strings with polars ("x" + pl.Series(labels).cast(pl.String)), but .to_list() still makes one Python str per label, and every solver binding then converts them again. Gurobi (addMVar(name=...), setAttr("ConstrName", ...)) pays a similar ~100 ms.

Suggestions
  • Avoid the getLp/passModel round trip in to_highspy: build a highspy.HighsLp once, with the matrix, bounds and names, and call passModel a single time. Or set the names without copying the LP out.
  • Make names cheaper or lazy: the default x{label}/c{label} names carry no information beyond the index. The solution is already mapped back by position, so the names could default off for the direct APIs, or be set only when needed (e.g. writing a file from the solver object, explicit_coordinate_names=True).
  • If names stay on by default, build them in one vectorised pass (numpy char/StringDType or polars) and pass them in the form each binding converts fastest, instead of going through list[str] twice.
Minimal reproducible example

Needs the freeze=True sparse path from #974 for the frozen constraints; the name cost is the same with dense constraints. Run with uv run python repro.py.

import time
import numpy as np, pandas as pd, xarray as xr
import linopy
from linopy import Model
from linopy.io import get_printers_scalar

linopy.options["semantics"] = "v1"
m = Model()
gen = pd.Index(range(2000), name="gen"); snap = pd.Index(range(100), name="snapshot")
rng = np.random.default_rng(0)
bus = xr.DataArray(rng.integers(0, 200, 2000), coords=[gen], name="bus")
p = m.add_variables(0, 10, coords=[gen, snap], name="p")
demand = xr.DataArray(rng.uniform(1, 5, (200, 100)), coords=[pd.Index(range(200), name="bus"), snap])
m.add_constraints((1.0 * p).groupby(bus).sum(sparse=True) == demand, name="balance", freeze=True)
m.add_constraints(p.diff("snapshot") <= 3, name="ramp", freeze=True)
m.add_objective((xr.DataArray(rng.uniform(1, 5, 2000), coords=[gen]) * p).sum())
M = m.matrices
print(f"vars={len(M.vlabels):,} cons={len(M.clabels):,} nnz={M.A.nnz:,}")

def best(f, n=5):
    ts = []
    for _ in range(n):
        t = time.perf_counter(); f(); ts.append(time.perf_counter() - t)
    return min(ts) * 1e3

print(f"to_highspy(set_names=True)   {best(lambda: linopy.io.to_highspy(m)):6.1f} ms")
print(f"to_highspy(set_names=False)  {best(lambda: linopy.io.to_highspy(m, set_names=False)):6.1f} ms")
pv, pc = get_printers_scalar(m)
print(f"  name strings               {best(lambda: (pv(M.vlabels), pc(M.clabels))):6.1f} ms")
h = linopy.io.to_highspy(m, set_names=False)
cn, rn = pv(M.vlabels), pc(M.clabels)
def assign():
    lp = h.getLp(); lp.col_names_ = cn; lp.row_names_ = rn
print(f"  getLp + assign names       {best(assign):6.1f} ms")
print(f"  passModel(getLp()) trip    {best(lambda: h.passModel(h.getLp())):6.1f} ms")
print(f"to_gurobipy names/no names   {best(lambda: linopy.io.to_gurobipy(m), 3):6.1f} / "
      f"{best(lambda: linopy.io.to_gurobipy(m, set_names=False), 3):6.1f} ms")
Output (HiGHS 1.15.1, gurobipy restricted license, Python 3.13)
vars=200,000 cons=218,000 nnz=596,000
to_highspy(set_names=True)    117.2 ms
to_highspy(set_names=False)    47.4 ms
  name strings                 27.7 ms
  getLp + assign names         13.3 ms
  passModel(getLp()) trip      11.2 ms
to_gurobipy names/no names    566.4 / 466.6 ms

The HiGHS banner lines are left out. The name strings, name assignment and round trip add up to about 52 ms of the about 70 ms difference; the rest is allocation/GC overhead from the extra Python strings.

Lingua principale
Python
Stelle
257
Fork
87
Merge medio
21h 29m
PR unite (30g)
42

Preparare l'ambiente

Come iniziare

  1. Leggi tutta la issue e poi la guida ai contributi del progetto.
  2. Commenta sulla issue per dire che te ne occupi tu — evita che due persone facciano lo stesso lavoro.
  3. Fai un fork del repository e lavora su un branch.
  4. Apri una pull request che faccia riferimento al numero della issue.

Altre issue di PyPSA/linopy

Tutte le issue di PyPSA/linopy

Issue simili

Altre issue su Python

Ricevi le nuove issue nella tua casella

Un breve riepilogo di issue GitHub adatte ai principianti.