You signed in with another tab or window. Reload to refresh your session.You signed out in another tab or window. Reload to refresh your session.You switched accounts on another tab or window. Reload to refresh your session.Dismiss alert
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.
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.
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:
to_highspy(m)(names on, the default)to_highspy(m, set_names=False)print_variables+print_constraints(build Pythonlist[str])h.getLp()+ assigninglp.col_names_/lp.row_names_(list tostd::vector<string>)h.passModel(lp)round tripto_gurobipy(m)vsto_gurobipy(m, set_names=False)So names make
to_highspyabout 2.4x slower. InHighs._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, andh.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 Pythonstrper label, and every solver binding then converts them again. Gurobi (addMVar(name=...),setAttr("ConstrName", ...)) pays a similar ~100 ms.Suggestions
to_highspy: build ahighspy.HighsLponce, with the matrix, bounds and names, and callpassModela single time. Or set the names without copying the LP out.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).char/StringDTypeor polars) and pass them in the form each binding converts fastest, instead of going throughlist[str]twice.Minimal reproducible example
Needs the
freeze=Truesparse path from #974 for the frozen constraints; the name cost is the same with dense constraints. Run withuv run python repro.py.Output (HiGHS 1.15.1, gurobipy restricted license, Python 3.13)
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.