Skip to content

feat(api): a written LP or MPS file can name each variable and constraint by its declaration and coordinate - #1792

Merged
FBumann merged 5 commits into
feat/pyomo-modelfrom
claude/festive-galileo-9481hq
Sep 30, 2026
Merged

FBumann merged 5 commits into
feat/pyomo-modelfrom
claude/festive-galileo-9481hq

Conversation

@FBumann

@FBumann FBumann commented Sep 30, 2026 •

Copy link
Copy Markdown
Collaborator

Prompt: I think we should add the names lp and mps file io first. It’s super cheap (a flag)

Note

The following content was generated by AI.

sps.write(..., names=True) and Model.write(path, names=True) write p(0,wind) and balance(0) in place of x0 and c0, in both LP and MPS files. The default is unchanged, byte for byte.

What a name is, and why
  • Form: declaration(label,label). A declaration with no dims is total(). A bare name can be an LP keyword: HiGHS and Gurobi both fail on a scalar named end, st or bounds. HiGHS also misreads free and inf.
  • Label cleaning: a label keeps \w (Unicode letters, digits, _) and !"#$%&'.;?@`{|}~. Every other character becomes _. The set is what HiGHS's LP reader takes back, probed character by character. Gurobi reads the same set, and non-ASCII letters, back. MPS takes everything except a space, but one rule keeps the two files naming a model the same way. (, ) and , belong to the name, so a label cannot contain them.
  • Collisions are refused. a b and a_b both clean to a_b. A reader would merge the two columns into one, or refuse the duplicate row, so the write raises SpecsolveError and names both coordinates.
  • Precedent: linopy's explicit_coordinate_names writes p[0,wind] with brackets changed to parentheses. It appends #label for uniqueness and cleans only spaces in a label. This PR drops the #label, so JuMP's variable_by_name finds a name without knowing the solver index. The refusal replaces it as the uniqueness guarantee.
  • Structure: Declared, in sinks/handoff.py beside Handoff, says which declaration owns each column and row, at which coordinate. readback.declared builds it from the label frames. readback.file_names spells each run's coordinate frame with polars expressions into a Names value (in writers/base.py, numbered by default), which both writers look names up in by solver index. The architecture page's table says where Declared lives.
Base of a stack

#1793 (the pyomo export) stacks on this PR and reads the same Declared, adding the SOS-set runs only it needs. Merge this PR first.

This branch carried the pyomo export for a while, when the stack went the other way. The last commit removes it again, and the diff against main holds only the named files and Declared.

Verification
  • On this tree (the head commit 51b1ad1 has the same tree as 65b4db2): pixi run check (lint, format, pyrefly, and the suite: 4662 passed, 522 skipped, 1 xfailed) and pixi run docs-build, both clean. pixi run test-floors (polars 1.30) and docs-test ran on the first commit.
  • Numbered output unchanged: before/after cmp on the LP and MPS files of the dispatch and MILP fixtures: identical.
  • tests/test_named_files.py:
    • Every fixture and every port (the ported models in examples/ports/) reaches the same optimum through a named LP and MPS file.
    • Every row HiGHS reads back by name holds the terms Model.row reads at that coordinate. That includes a masked variable and a constraint whose middle row is dropped, so later rows renumber.
    • On dispatch (unique optimum), each named column reads back the engine's primal value.
    • Label cleaning, datetime labels, keyword-named scalars and the collision refusal on a variable and on a constraint.
    • Named SOS sets and a quadratic row, read by Gurobi.

Mutation table (full suite each time; the tree came back clean after every row). The first three rows were taken on af3dc44, whose file_names and guards this head carries unchanged:

mutation result
the shared-name refusal (tools.mutate, readback.py:164) caught
drop the label cleaning replace_all (by hand) caught (14 failed)
a bare name for a dimensionless declaration (by hand) caught (4 failed)
names follow declaration order, not build order (by hand, first commit) caught by the purpose-built probe test_a_quadratic_constraint_declared_first_keeps_its_names

Sorting each label frame and sorting the declarations by start both survived the suite, so both sorts were removed. Declared relies on the same order, which the probe above holds.

Not done or not checked
  • JuMP not run: there is no Julia in this environment. The claim that JuMP reads these names rests on the character set HiGHS and Gurobi read.
  • No length limit: CPLEX and Gurobi cap a name at 255 characters, and this PR adds no guard for it.
  • Datetime labels are long: a datetime label is written in polars' string form, for example p(2030_01_01_06_00_00.000000). It is faithful and unique, but long.
  • SOS set names stay s0, s1.
  • Write time not measured: the time cost of named output was not measured.

🤖 Generated with Claude Code

https://claude.ai/code/session_01CM4oeTkzkk5G5zE6zgikSE

…aint by its declaration and coordinate

`names=True` on `sps.write` and `Model.write` writes `p(0,wind)` and
`balance(0)` in place of `x0` and `c0`. A label keeps the characters LP
and MPS readers take back and writes the rest as `_`; a dimensionless
declaration is `total()`, so a keyword never stands alone as a name. Two
coordinates that clean to one name are refused, naming both.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01CM4oeTkzkk5G5zE6zgikSE
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01CM4oeTkzkk5G5zE6zgikSE
@codspeed

codspeed Bot commented Sep 30, 2026 •

Copy link
Copy Markdown

Merging this PR will not alter performance

✅ 24 untouched benchmarks
⏩ 82 skipped benchmarks1


Comparing claude/festive-galileo-9481hq (51b1ad1) with feat/pyomo-model (a126491)

Open in CodSpeed

Footnotes

  1. 82 benchmarks were skipped, so the baseline results were used instead. If they were deleted from the codebase, click here and archive them to remove them from the performance reports. ↩

@read-the-docs-community

read-the-docs-community Bot commented Sep 30, 2026 •

Copy link
Copy Markdown

Documentation build overview

📚 specsolve | 🛠️ Build #34854235 | 📁 Comparing 51b1ad1 against latest (af1779e)

  🔍 Preview build  

4 files changed
± about/architecture/index.html
± about/changelog/index.html
± howto/solvers/index.html
± reference/api/index.html

The two branches both knew which declaration owns which column and row
at which coordinate. Merged, `file_names` reads `readback.declared`, the
one home #1793 puts beside the handoff, and keeps its label cleaning as
polars expressions over each run's coordinate frame.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01CM4oeTkzkk5G5zE6zgikSE
@FBumann
FBumann changed the base branch from main to feat/pyomo-model September 30, 2026 10:56
@FBumann
FBumann added this pull request to stack #1794 September 30, 2026 10:57
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01CM4oeTkzkk5G5zE6zgikSE

# Conflicts:
#	src/specsolve/relational/engines/polars/engine.py
@FBumann
FBumann removed this pull request from stack #1794 September 30, 2026 14:13
The named LP/MPS files land first. This branch keeps them and the
`Declared` runs their names are spelled from, which own each column and
row; the pyomo export, and the set runs only it reads, move to
feat/pyomo-model, stacked on this branch.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01CM4oeTkzkk5G5zE6zgikSE
@FBumann
FBumann merged commit cfcc5b2 into feat/pyomo-model Sep 30, 2026
12 checks passed
@FBumann
FBumann deleted the claude/festive-galileo-9481hq branch September 30, 2026 14:20
@FBumann
FBumann restored the claude/festive-galileo-9481hq branch September 30, 2026 14:21
FBumann pushed a commit that referenced this pull request Sep 30, 2026
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