Skip to content

refactor(api): a linopy model is built by linopy rather than by this package - #1541

Closed
FBumann wants to merge 1 commit into
mainfrom
claude/modest-ritchie-6ek8am
Closed

FBumann wants to merge 1 commit into
mainfrom
claude/modest-ritchie-6ek8am

Conversation

@FBumann

@FBumann FBumann commented Sep 8, 2026

Copy link
Copy Markdown
Collaborator

Prompt: As linopy now implements mathspec, should we drop the linopy lane from lpspec fully?

Prompt (scope): Let's do a PR that assumes the linopy [PR is] merged and pin its head — the whole drop in one PR.

Note

The following content was generated by AI.

src/lpspec/linopy/ is deleted and the differential oracle is now
linopy.Model.from_spec, pinned at PyPSA/linopy#922's
head 8b88ac72. This cannot merge as it stands: that PR is a draft, and its
build refuses nine of the ten ported models over a rule math-spec decides the
other way.

The blocker, first

linopy's port made absence uniform — a missing parameter row is refused
wherever it is used. Rule 8
says a missing row reads as the value that contributes nothing, which for a
coefficient is 0, and refuses only where no such value exists: a divisor,
a bound, a constant side, a piecewise breakpoint. So the oracle refuses the
language's own sparsity idiom.

A second, smaller one: a dtype: datetime dimension's labels are canonicalised
against the declared dtype here (#1076) and compared as handed over there, so a
datetime.date index and a datetime64 lookup value are two labels to the
oracle and one to this engine.

Both are recorded as strict xfails naming the divergence — 20 of them,
against spec_oracle.SPARSE_COEFFICIENT and spec_oracle.DATETIME_SPELLING —
so each XPASSes and the constant comes out the day it is fixed upstream. Nine
of those are the whole of test_corpus_parity's ORACLE_GAPS table. Until
they are fixed, this PR trades a working in-repo oracle for one that does not
run on most of the corpus, which is the reason not to merge it yet rather than
a detail of it.

What is in the diff

  • Deleted: lpspec/linopy/ (1550 lines, 8 modules), lanes.py (Buildable
    moves to api.py, LANES had one entry and goes), capabilities.lane_cannot_build_message,
    check(spec, sink='linopy'), and tests/test_linopy_lane.py (941 lines).
  • The oracle: tests/spec_oracle.py — two verbs keeping the deleted lane's
    signatures over Model.from_spec / model.spec.evaluate, plus
    linopy_sources, which respells this package's tables into linopy's
    containers and validates nothing, so a defect under test reaches
    linopy.spec.attach and comes back in linopy's words. tests/oracle.py
    keeps the [linopy] guard and grows a second one for Model.from_spec.
  • both_lanes_refuse → both_refuse: it no longer compares the two
    sentences. That claim was only ever true because both lanes read through one
    tidy_sources; linopy has its own reader now, so what is checkable is that
    both refuse and neither calls a data defect a language error.
  • test_data_parity's verdict is REFUSED/ACCEPTED, not an exception
    class — each package raises its own.
  • The pin: linopy @ git+…@8b88ac72, a rev rather than master. That
    reverses the rule the pin carried, and the comment says why: the oracle is
    linopy's implementation of the same language now, so its math-spec and ours
    must be the same one, and master moving would swap the oracle's language
    under a suite that pins its own. The pixi.lock comment that leaned on
    "the extra resolves a branch" is updated with it.
  • differential/pypsa/parity.py builds through the same module (it imports
    tests.spec_oracle directly — it is not collected by pytest and has no bare
    install to run on).

Coverage that moved, and coverage that is gone

what where it went
test_linopy_lane.py — the lane's builder, loader, operators, where, absence helpers deleted; the same ground is linopy's own test/test_spec_*.py
test_sum_by_lookups's direct operator_grouped_sum probe deleted — it called a function that no longer exists
test_architecture.test_the_linopy_lane_stays_two_verbs deleted with the surface it pinned
test_architecture.test_both_lanes_dispatch_on_every_plan_node kept as test_the_engine_dispatches_on_every_plan_node; the cross-lane half is the differential's now
test_docs_site.test_the_translation_table_names_every_built_in_operator deleted — it checked a copy of the deleted builder
the lane fence (_in_linopy_lane, the exemption in two import rules) stronger: no module in the package may import linopy or xarray at all, with nothing left to exempt
check(spec, sink='linopy') gone. Capability answers for sinks only
the LaneError naming the way round a quadratic constraint gone; a caller reaching linopy directly gets its TypeError. test_linopy_cannot_build_one_and_this_engine_can pins that

Verified

ruff check · ruff format --check · pyrefly check · pytest -n 8 ·
mkdocs build --strict — all clean. Run in a uv venv rather than pixi (no
pixi in this environment), against the pinned linopy at 8b88ac72.

Not run: pixi run test-floors (the bare-install shape), test-bench, and
the PyPSA parity workflow — none has pypsa or a pixi environment here, so
differential/pypsa/parity.py's repoint is unexercised. The
docs/examples/pypsa_ladder.md column rename is applied to the page and to
tools/ladder.py in lockstep, but the page has not been regenerated.

Suite
3528 passed, 251 skipped, 20 xfailed in 93.16s

The 20 xfails are the two divergences above: 9 corpus models, 4 in
test_absence/test_arithmetic_laws, 2 in test_piecewise, 2 in
test_reserves, 3 datetime cases in test_data_parity, and the sparse
coefficient row of its verdict table.

Departures from Part 2, and what I did not do

  • No mutation table. The diff deletes guards rather than adding them; the
    one correctness guard it touches (tests/oracle.py's second guard) is a
    refusal to run at all, not a branch.
  • tests/spec_oracle.py is a new module with one job — the shim rule in
    Part 2 says no new layer without something concrete needing it now. Two
    callers need it now (the suite and the PyPSA harness) and the two packages
    genuinely spell sources differently.
  • IN_BREACH 214 → 209 in tests/test_assertions.py, following the
    deleted files.
  • I did not rename the [linopy] extra. After the drop its user-facing
    content is pandas and xarray for Result.to_pandas / to_dataarray, and
    linopy is a test dependency; renaming it touches pypsa-parity.yml, the pixi
    features and the pin tests, and is separable.
  • I did not file the two divergences upstream. They belong on
    Build and change models via math-spec PyPSA/linopy#922 and are the merge blocker.

🤖 Generated with Claude Code

https://claude.ai/code/session_01KmAEXeq2gC4eFbr7Yeur74


Generated by Claude Code

…package

linopy reads math-spec natively, so the eager lane in `lpspec/linopy/` is a
second, smaller implementation of something linopy owns. It goes, and what
took its place as the differential oracle is `linopy.Model.from_spec`.

The `[linopy]` extra now pins a rev rather than master: the oracle and this
package must lower with the same math-spec, and a moving branch would swap
the oracle's language under a suite that pins its own.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01KmAEXeq2gC4eFbr7Yeur74
@read-the-docs-community

Copy link
Copy Markdown

@FBumann FBumann mentioned this pull request Sep 25, 2026
19 of 33 tasks
FBumann added a commit that referenced this pull request Sep 25, 2026
> **Prompt:** And prepare a todo list for me the same way. Also start
with 0.1.0rc1

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

This is the release PR for `0.1.0rc1`, the release candidate for the
first PyPI upload. It is stacked on #1751. It stays a draft until PyPI
can take the package. One direct reference is left, the `linopy` extra,
and #1755 removes it (step 1).

## The first PyPI release, step by step

### 1. What blocks the upload

PyPI refuses a distribution that names a git URL in its metadata.

- [x] mathspec is on PyPI, and `pyproject.toml` depends on
`mathspec>=0.1.0` (#1754). The parity job reads the corpus tag from that
floor.
- [x] Merge #1755. It takes linopy out of the package and keeps it as
the test oracle: the `linopy @ git+…@master` reference moves to the
`dev` group, which PyPI never sees. It also deletes
`allow-direct-references`, so hatchling refuses a direct reference at
build time, before PyPI would.

### 2. One-time setup

**PyPI**
- [x] On pypi.org, go to Account → Publishing → "Add a new pending
publisher" → GitHub, and fill in:
  - PyPI project name: `specsolve`
  - Owner: `fluxopt`
  - Repository name: `specsolve`
  - Workflow name: `release.yaml`
  - Environment name: `pypi`

**GitHub**, under fluxopt/specsolve Settings
- [x] Environments: create or edit `pypi`. Under "Deployment branches
and tags", choose "Selected branches and tags" and add `main`. Add the
people who may approve a release as required reviewers.
- [x] After #1751 merges: Rules → the `main` ruleset. Require `ci`,
`Conventional commit subject` and `Changelog line`. A required check
must run on `main` once before you can require it.
- [x] Issues → Labels: create `no changelog`.

**Clean-up that the release does not need**
- [ ] Delete the repository variables `AUTO_RELEASE` and
`PUBLISH_TO_PYPI` if they exist. Turn off "Allow auto-merge" unless you
want it for other PRs.
- [x] Remove `specsolve` from the release app's (`fluxopt-release-bot`)
repository access. If no other repository uses the app, uninstall it and
delete the secrets `APP_CLIENT_ID` and `APP_PRIVATE_KEY`.
- [x] Delete the branches
`release-please--branches--main--components--farkas` and
`release-please--branches--main--components--linopy-yaml`.
- [x] Close #1675 and #1676. #1751 replaces both.
- [ ] Open PRs owe a changelog line after #1751 merges: #1737, #1527,
#1516 and #1541. #1541 (drop the linopy lane for linopy's own
`from_spec`) conflicts with #1755; close it or rebase it.

### 3. Before this PR merges: what the PyPI page shows

- [x] `[project.urls]` has only `repository`. Add `Documentation`,
`Issues` and `Changelog`, as energy-models/mathspec#707 did.
- [x] The README has two relative links, `CONTRIBUTING.md` and
`docs/about/prior-art.md`. On PyPI they do not resolve. Make them
absolute.
- [x] `src/specsolve/relational/parquet.py` has two error messages that
say "while the package is on 0.0.1aN". `tests/test_archive.py` asserts
the same. Reword them to "before 1.0".
- [x] RELEASING.md, "Relaxed while in early development": it says to
tighten CI before 0.1.0, starting with a Python matrix behind the gate
job. Do that, or change the sentence.

### 4. This PR

- [x] Merge #1751, then #1755. Then this PR's base must be `main`.
GitHub retargets it when #1751's branch is deleted; if the branch stays,
retarget by hand.
- [x] Mark the PR ready.
- [x] Set the date in `## 0.1.0rc1 (2026-09-25)` to the day you merge,
if that is not the 25th.
- [x] Edit the notes under the heading. Add the lines of the PRs merged
since. The text becomes the GitHub release notes, as written.
- [x] Check that `ci` is green. Its step "Check the changelog headings"
prints `releases 0.1.0rc1 on merge`.
- [x] Review, then squash-merge.

### 5. After the merge

- [ ] Actions → **Release**, the run for the merge commit:
- [ ] `Tag the version the changelog names` created the tag `v0.1.0rc1`
and the GitHub release `0.1.0rc1`, marked as a pre-release.
  - [ ] `Build` passed, and its check printed `specsolve-0.1.0rc1`.
- [ ] `Publish to PyPI` waits for approval: "Review deployments" →
`pypi` → Approve.
- [ ] https://pypi.org/project/specsolve/0.1.0rc1/ exists. The README
renders, and the project links work.
- [ ] Smoke test in a fresh environment:
  ```bash
  python -m venv /tmp/ss && /tmp/ss/bin/pip install specsolve==0.1.0rc1
/tmp/ss/bin/python -c "import specsolve, mathspec;
print(specsolve.__version__, mathspec.__version__)" # 0.1.0rc1 0.1.0
/tmp/ss/bin/pip install specsolve # finds no stable version until 0.1.0
exists
  ```
- [ ] If a job failed, fix the cause, then use "Re-run failed jobs" on
that run. If `0.1.0rc1` reached PyPI broken, release `0.1.0rc2`.

### 6. Then 0.1.0

- [ ] Open the next release PR, titled `chore: release 0.1.0`. It
renames the next `## Upcoming version` to `## 0.1.0 (date)`, with notes
that say this is the first release on PyPI. Merge it and approve the
upload as above.
- [ ] Smoke test `pip install specsolve` without a version. It should
install `0.1.0`.

### 7. Follow-ups

- [ ] 83 links in the tree point at `math-spec.readthedocs.io`. The
redirect keeps them working. Point them at `mathspec.readthedocs.io`.
- [ ] Optional: a conda-forge recipe.
- [ ] Patch releases to an older version line. No path exists yet;
mathspec tracks the same gap in energy-models/mathspec#710.

<details><summary>What this PR changes, what merging does,
gates</summary>

**The changes:**
- `CHANGELOG.md`: an empty `## Upcoming version`, then `## 0.1.0rc1
(2026-09-25)`, with a paragraph and #1748's line.
- `AGENTS.md`: "The project is `0.0.1aN` and holds no compatibility
promise" becomes "The project holds no compatibility promise before
1.0". One sentence is added: a release that breaks a model file or an
import raises the minor version, and its notes name the break.
- `CONTRIBUTING.md`: the same rule, in the same words.

**Why a release candidate:** it goes through the same tag, GitHub
release, build, approval and trusted-publishing upload as `0.1.0`. The
only addition is `--prerelease` on the GitHub release. pip skips it
unless someone asks for it, so a broken first upload reaches no user.
The version sorts after `0.0.1a358` and before `0.1.0`.

**What merging does:** `release.yaml` finds `0.1.0rc1` with no tag. It
creates `v0.1.0rc1` and a GitHub pre-release, builds from the tag,
checks that the wheel and the sdist are `0.1.0rc1`, and waits for
approval on `pypi` before the upload.

**Gates:** `python -m tools.changelog check` prints "releases 0.1.0rc1
on merge", and `notes 0.1.0rc1` prints the section above. The suite on
#1751's commit is in that PR. This commit changes only prose and the
changelog.

**Not verified:** the name `specsolve` on PyPI. The PyPI API cannot be
reached from this container, so check that the name is free when you add
the pending publisher.

The type is `chore`, which owes no changelog line.

</details>

🤖 Generated with [Claude Code](https://claude.com/claude-code)

https://claude.ai/code/session_016Pv7LzSgzt7Yn2K3ioyJXw

Co-authored-by: Claude <noreply@anthropic.com>

FBumann commented Sep 25, 2026

Copy link
Copy Markdown
Collaborator Author

Note

The following content was generated by AI.

Closed as overtaken by #1755. That PR removed the src/ half of this change: specsolve no longer builds a linopy model. It kept tests/linopy_lane/ as the differential oracle. The other half swaps that oracle for linopy's Model.from_spec. No linopy revision has from_spec for mathspec 0.2.0: linopy master has no from_spec, and its spec branches pin old math-spec alphas. File the swap again once linopy supports mathspec ≥ 0.2.0.


Generated by Claude Code

@FBumann FBumann closed this Sep 25, 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