Skip to content

feat(api): a sweep can be cut by several axes at once, each carrying its own state, such as a rolling horizon per scenario or a pathway whose decades roll through their hours - #1888

Draft
FBumann wants to merge 125 commits into
mainfrom
claude/wizardly-lamport-e3g6om-chains
Draft

FBumann wants to merge 125 commits into
mainfrom
claude/wizardly-lamport-e3g6om-chains

Conversation

@FBumann

@FBumann FBumann commented Oct 8, 2026 •

Copy link
Copy Markdown
Collaborator

Prompt: Is our api and architecture suited to extend to solver multiple Axes at once? rolling horizon per scenario etc?

Prompt: If generalizing and refactoring the code makes for the better end result, we can do it!

Prompt: Not sure if this api is intuitive and flexible enough? WHat about slicing per scenario AND period? Or doing 2 Windows together?

Prompt: Can we combine them into one PR?

Note

The following content was generated by AI.

solve_over takes a tuple of axes, outer first, each with its own carry= (#1980), and any of them may be windows. A carry reaches its axis's next label; an executor runs independent chains concurrently. Each chain answers exactly as its own sweep would.

sweep = sps.solve_over('window.yaml', sources, (
    sps.EachCoordinate('scenario'),
    sps.EachWindow('snapshot', steps=24, lookahead=24, into='t', carry={'soc_initial': 'soc'}),
))
sweep.primal('soc')    # (scenario, snapshot, value)

pathway = sps.solve_over('pathway.yaml', sources, (
    sps.EachWindow('period', steps=1, lookahead=1, into='y', carry={'existing': 'capacity'}),
    sps.EachWindow('snapshot', steps=24, lookahead=24, into='t', carry={'soc_initial': 'soc'}),
))
pathway.primal('soc')  # (period, snapshot, value)
What changed
  • A tuple of axes cuts with each in turn, outer first. Two axes over one dimension, two windows over one local index, a non-axis in the tuple, and key_name= with a tuple are refused before a slice is built.
  • Each axis carries its own state. A carry reaches the slices of its axis's next label. Under an axis inside it, the value is the last inner slice's, and it reaches every inner slice of the next label: a period hands the next period the fleet its last window ended with. An inner carry restarts from its seed at each outer label. Two axes writing one parameter are refused. A window's carry reads its own axis's seam, so Slice.owns holds one count per axis.
  • A chain is the slices that share the keys of every axis outside the outermost one that carries. Under an executor, where an axis carries, each chain is one task, run in order on one model, so 'previous' (refactor(api): keep= is gone, and a re-solve carries on from where the last solve ended unless start=None says otherwise #1884) carries on along it; otherwise each slice is one task. A sweep of one chain still refuses an executor with a carry.
  • One stitch spans every window axis. The answer is over every windowed dimension and keeps only what every window owns: an hour the decade window solved only as lookahead is not in it. Per window, it keeps every key and every local index.
  • The key is a tuple. Slice.key is a tuple of labels, KeyColumns carries the key columns and their types through the spill, the record and the archive. Sweep.key_names is new; Sweep.key_name is the last. Sweep.keys holds labels for one axis and tuples for several. On disk, slice_axis and slice join names and labels with /.
  • A tuple is plain data too, as feat(api): an axis is written as plain data with to_dict, and solve_over takes that data in its place #1981 makes one axis: {'each': 'axes', 'axes': [each.to_dict(), ...]}. solve_over takes it in the tuple's place, and a nested sweep's axis.json is it. One axis writes exactly what main writes.
  • Spill, resume, archive, keep_windows, per_window, start= an earlier sweep, and an undeclared expression off an archive all work over a tuple; load_archive and load_inputs give the tuple back as .axis. The archive's uncut sources drop every window's local index.
  • ANSWER_LAYOUT rises from 4 to 5 (the spill manifest names key_names, and the stitch its locals, dims, keys and outer keys), once since 0.8.1. INPUTS_LAYOUT stays 1: one axis writes the axis.json main writes, so every 0.8.1 archive's spec and sources still read; a tuple's axis.json is new, and 0.8.1 cannot read it.
  • Docs: a "Several axes" section in the sweeps reference, with the pathway example, and its rules on reading, carrying, starting and running; Chain in the glossary; the parallel how-to.
Merged with main: #1884, #1980 and #1981
Breaking, and coverage that moved
  • Slice.key is a tuple, and Slice.owns one count per axis.
  • A 0.8.1 spill or archive answer is refused by ANSWER_LAYOUT; load_inputs still reads a 0.8.1 archive's spec and sources.
  • The windows-last refusal case window-outside became one-local-index-for-two-windows. The no-each refusal now lists axes among the kinds.
Tests

tests/test_nested_sweep.py. The oracle is the loop it replaces.

  • Each scenario's answer and objectives equal its own carried sweep. The store is seeded at 50 and every window empties it, so a carry crossing chains would change the answer.
  • Two carrying axes: a pathway of two periods, each a carried rolling horizon. Each period equals its own horizon with existing copied by hand from the previous period's last window; 2040's first window differs from 2030's, so the fleet did arrive.
  • Windows inside windows: decades with a one-decade lookahead, hours rolling inside each. The answer holds each (period, snapshot) once, the lookahead decade dropped; per window it keeps both keys and both locals. With the fleet carried, each decade window equals its own hourly horizon over its decade and the next, existing set by hand; every window's objectives match.
  • Thread and spawned process pools give the serial answer, each chain built once; an outer carry makes the whole pathway one chain, so an executor is refused.
  • Spill and resume without a build; archive with windows, re-run from archive.axis, evaluate an undeclared expression; start= an earlier nested sweep gives each window its own rows. A nested axis.json is the tuple's plain data, reads back as the tuple, and solves to the same answer.
  • Three axes, two coordinate axes, a parameter two axes carry, the tuple refusals, and two malformed tuple mappings.

Where a start is in play, tests compare objectives: this LP has several optimal vertices, and a start can end on another.

Mutation table

python -m tools.mutate for the deletions, scoped to tests/test_nested_sweep.py and tests/test_strategy.py; hand mutations by sed, restored with git checkout --, __pycache__ dropped on both sides. Rows 1–13 were taken on 1de4d33 and rows 14–16 on 507123e, before main was merged in, so their line numbers are those commits'; row 17 on e1a9ebb. Clean after every run.

guard result
a tuple holding something other than an axis (axes.py:337-341) caught
a window outside another axis (axes.py:342-347), since replaced by row 14 caught
two axes over one dimension (axes.py:349-353) caught
carry with an executor on one chain (strategy.py:380-385) caught
key_name= with a tuple of axes (strategy.py:962-966) caught
a parameter two axes carry (strategy.py:197-201) caught
an outer label change resets the inner carry (strategy.py:719) caught
by hand: a chain is one task (every slice its own task); taken again on 8736754 caught (3 failed)
by hand: the stitch joins on the outer keys (on=keys) caught (4 failed)
by hand: the archived record drops every key column caught (2 failed)
by hand: an axis hands on only at the end of its label caught (1 failed)
by hand: an outer carry reaches the next label (outer carries never hand on) caught (1 failed)
by hand: a chain starts at the outermost carrying axis (always the last axis) caught (1 failed)
two windows over one local index (axes.py:354-358) caught
by hand: an inner coordinate is owned only under the outer window that owns its decade (join dropped) caught (2 failed)
by hand: a window carry reads its own axis's seam (owns[-1]) caught (1 failed)
a tuple mapping that is not each and a list of axes (axes.py:524-528) caught
Verification
Deliberately not done
  • Renaming solve_over to solve_sweep: its own PR.
  • Resuming part of a chain under an executor: a chain the spill holds only part of runs whole again; serially it resumes from the first unfinished slice.
  • Reading one outer window as a whole. per_window=True returns the innermost windows, keyed by every window key.
  • A number over two local indexes at once in an archive's uncut sources is spread over each window's dimension in turn, not over both; no test covers it.
  • A tuple whose axes are themselves mappings. The tuple's plain data is one mapping; a Python tuple takes axes.

🤖 Generated with Claude Code

https://claude.ai/code/session_014wLrXpeSGDqoBFxyrs8NEz

claude added 30 commits October 6, 2026 21:20
…sks for it, and a sweep can hold the activity too

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01GLBvwp6NU7YjszFz5QMRf1
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01GLBvwp6NU7YjszFz5QMRf1
…ith the same sign on every solver

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01GLBvwp6NU7YjszFz5QMRf1
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01GLBvwp6NU7YjszFz5QMRf1
…hive carry them

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01GLBvwp6NU7YjszFz5QMRf1
…ar each constraint is from binding

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01GLBvwp6NU7YjszFz5QMRf1
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01GLBvwp6NU7YjszFz5QMRf1
…s no activity

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01GLBvwp6NU7YjszFz5QMRf1
… is found by its kind's name

The engine computes each output asked for in one place instead of threading
the activity through the read-back. A sweep slice collects the frames the
result already holds rather than re-reading them per name. Result and Sweep
find a kind's reader by its name, so a new output adds no dispatch entry.
The read-only wrappers around three module tables are dropped.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01UFX6qbCYnXKTVkucgxEqfS
…puts' into claude/festive-carson-8x46sq

# Conflicts:
#	src/specsolve/relational/engine/engine.py
#	src/specsolve/relational/result.py
#	src/specsolve/strategy.py
#	src/specsolve/sweep.py
…gives the duals' reason

Strict xfail: today the bridge lists no names and fails unpacking them.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01UFX6qbCYnXKTVkucgxEqfS
…the duals' reason

The duals' reason is given where an output is read, so the bridges and the
reader share it rather than the reader alone.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01UFX6qbCYnXKTVkucgxEqfS
…n-8x46sq-slack

# Conflicts:
#	src/specsolve/relational/engine/engine.py
#	src/specsolve/relational/result.py
#	src/specsolve/sweep.py
…ow that activity is laid out as an output

Activity is laid out by the same per-constraint helper as the duals, so the
dual's label-order assertion covers it.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01UFX6qbCYnXKTVkucgxEqfS
…into review/1884-tidy

# Conflicts:
#	docs/reference/sweeps.md
Base automatically changed from claude/wizardly-lamport-e3g6om-keep to main October 8, 2026 14:16
claude added 7 commits October 8, 2026 14:25
start='previous' is now the default and carries the solver on, so a
chain no longer restarts it: each slice passes its own start to
Model.solve, and the basis read per slice goes. Under an executor a
chain is one task only where an axis carries; 'previous' alone no
longer groups slices, since it is the default and would leave a sweep
of one axis a single task. 'previous' with an executor is no longer
refused, and a start that leaves a slice no row warns rather than
refusing the sweep.

Tests: the chain-cold test goes with the behaviour, and the executor
test counts one build per chain. A main test moves carry= onto its axis.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_014wLrXpeSGDqoBFxyrs8NEz
Main holds the stack below as squash commits: its tree is #1884's last
head, merged in the commit before this one, plus #1893's test fix,
taken here.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_014wLrXpeSGDqoBFxyrs8NEz
…ort-e3g6om-windows

Brings main, which now holds the stack below, and the start= redesign:
'previous' is the default and carries the solver on.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_014wLrXpeSGDqoBFxyrs8NEz
…ort-e3g6om-windows

Brings main through #1888.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_014wLrXpeSGDqoBFxyrs8NEz
The column names specsolve writes now live in relational/names.py
(#1889); the stitch and the uncut archive read VALUE from there.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_014wLrXpeSGDqoBFxyrs8NEz
…ort-e3g6om-windows

Brings main through #1888: the column names now live in
relational/names.py (#1889), and the stitch over every window and the
uncut archive read VALUE from there.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_014wLrXpeSGDqoBFxyrs8NEz
@FBumann
FBumann removed this pull request from stack #1886 October 9, 2026 08:46
@FBumann
FBumann added this pull request to stack #1917 October 9, 2026 08:46
claude added 12 commits October 9, 2026 09:41
Main's answers now carry their spec rather than a digest, and an
archive no longer digests its sources (#1913, #1904): the spill writes
the spec beside the keys, the sweep folds it in, and a sweep archive
holds the spec, the sources, the axes and the answer. Main raised
ANSWER_LAYOUT to 5 and INPUTS_LAYOUT to 2 for that; this branch had
taken the same numbers for its own key and axis files, so they rise to
6 and 3, and a file in either earlier layout is refused. The tests'
dataclasses.replace import, which main dropped, comes back.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_014wLrXpeSGDqoBFxyrs8NEz
…ort-e3g6om-windows

Brings main through #1888: answers carry their spec (#1913), and the
layouts are ANSWER_LAYOUT 6 and INPUTS_LAYOUT 3.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_014wLrXpeSGDqoBFxyrs8NEz
Main released 0.8.0 with ANSWER_LAYOUT 4 and INPUTS_LAYOUT 1, one rise
each since 0.7.0. This branch's key and axis files rise them once more
since that release, to 5 and 2, and the save docstring's stamp follows.
The changelog takes main's 0.8.0 section; this PR's line stays under
Upcoming version.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_014wLrXpeSGDqoBFxyrs8NEz
…ort-e3g6om-windows

Brings main's 0.8.0 release through #1888: the layouts are
ANSWER_LAYOUT 5 and INPUTS_LAYOUT 2, and this PR's changelog line
stays under Upcoming version beside #1888's.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_014wLrXpeSGDqoBFxyrs8NEz
…ort-e3g6om-windows

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_014wLrXpeSGDqoBFxyrs8NEz
Main's archive now writes its answer once, through the writer the
archive hands it (#1922, #1924): sweep_manifest, Spill.write_frames and
Sweep._frames_by_slice are taken over every key column, so the archive,
the spill and the in-memory windows keep one column per axis. The
key columns read off a sweep's record in one place, Sweep._key_columns.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_014wLrXpeSGDqoBFxyrs8NEz
…ort-e3g6om-windows

Brings main's single-write archive through #1888: sweep_manifest now
names the stitch's locals, dims, keys and outer keys, as the manifest
this branch wrote by hand did.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_014wLrXpeSGDqoBFxyrs8NEz
…rt-e3g6om-chains

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_014wLrXpeSGDqoBFxyrs8NEz
…rdly-lamport-e3g6om-windows

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_014wLrXpeSGDqoBFxyrs8NEz
FBumann added a commit that referenced this pull request Oct 12, 2026
…lve_over, so an archive's axis holds its carry (#1980)

> **Prompt:** There is a pr that changes the axis classes to include the
carry
> Maybe we should extract this into main first

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

`carry=` moves from `solve_over` onto `EachCoordinate` and `EachWindow`,
as #1888 has it, for one axis. An archive's axis now holds its carry, so
`solve_over(spec, sources, archive.axis)` runs a sweep again.
`axis.json` is unchanged byte for byte.

<details><summary>What changed</summary>

- **`EachCoordinate(..., carry=)` and `EachWindow(..., carry=)`**, a
keyword-only `{parameter: variable}`, empty by default. `solve_over`
loses `carry=`.
- **A hand-built list carries nothing.** It has no axis to hold a carry.
No test used one.
- **`SweepArchive.carry` and `ArchivedInputs.carry` are gone.** The
carry is `archive.axis.carry`.
- **The carry stays out of the hash.** It is a mapping, so a frozen axis
with one would be unhashable otherwise. Equality still compares it. Axes
on `main` are hashable, and a new test
(`test_an_axis_that_carries_is_still_hashable`) holds that.
- The executor refusal, the seed check and the line-up checks are
unchanged. They read the carry off the axis.
- Docs: the sweeps reference, the sweep tour, the archiving how-to, the
glossary entry and both examples move to `carry=` on the axis.

</details>

<details><summary>Why no layout number rises</summary>

On `main`, the archive wrote the carry as a `carry` key beside the
axis's own keys in `axis.json`. The axis now writes the same key in the
same place. I wrote the same carried rolling horizon with released 0.8.1
and with this branch, and `cmp` reports the two `axis.json` files
identical:

```
{"each": "window", "dim": "snapshot", "steps": 4, "lookahead": 2, "into": "t", "carry": {"soc_initial": "soc"}}
```

So `INPUTS_LAYOUT` stays at 1, and an 0.8.1 archive reads back with its
carry on the axis.

</details>

<details><summary>Breaking, and where coverage moved</summary>

- `solve_over(..., axis, carry=c)` → `solve_over(..., EachWindow(...,
carry=c))`, or `replace(axis, carry=c)` for an axis held in a constant.
Every call in tests, examples and docs moved. The rewrite matches
#1888's (`4cb40c4c`) call for call, so #1888 merges `main` with few
conflicts.
- `archive.carry` → `archive.axis.carry`, and `inputs.carry` →
`inputs.axis.carry`. The two assertions that read them now read the
axis. `test_a_rolling_horizon_archive_reads_back_its_answer` now
compares against the axis with its carry.

</details>

<details><summary>Mutation table, gates, and what was not done</summary>

| mutation | result | tree |
|---|---|---|
| `axes.py`: `hash=False` dropped from both `carry` fields | caught:
`test_an_axis_that_carries_is_still_hashable[coordinate]` and
`[window]`, `TypeError: unhashable type: 'dict'` | clean |

I took it by hand: committed tree, `git checkout --` restore,
`__pycache__` dropped. Before the probe existed, the full suite passed
with this mutation (5605 passed). That is why the probe was added.

- **Gates.** `pixi run check`: lint, format and typecheck clean, 5605
passed, 566 skipped, 4 xfailed. `pixi run -e docs docs-build`: strict,
no issues; the sweep tour runs its examples. Not run: `docs-test`,
`test-bench`, `test-floors`.
- **Not done.** Several axes and chains stay in #1888. A public way to
save an axis (`to_dict`) is a separate PR.

</details>

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

https://claude.ai/code/session_01G1TmcQZZCctsVr54HE93qS

---
_Generated by [Claude
Code](https://claude.ai/code/session_01G1TmcQZZCctsVr54HE93qS)_

---------

Co-authored-by: Claude <noreply@anthropic.com>
FBumann added a commit that referenced this pull request Oct 12, 2026
…ver takes that data in its place (#1981)

> **Prompt:** And about the axis from: not sure what's the best api
here!! Maybe the dataclass could have a from and to json or yaml? Or
dict?

> **Prompt:** Stack it onto 1980

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

`EachCoordinate.to_dict()` and `EachWindow.to_dict()` write an axis as
plain data, carry included. `solve_over` takes that mapping in the
axis's place. An archive's `axis.json` is the same mapping, so the
browser app can solve from it without importing `specsolve.axes`.

<details><summary>The shape, and why it matches the rest of the
API</summary>

- **Write with a method, read where the value is taken.** This follows
the spec: mathspec's `Spec.to_dict()` writes it, and `to_spec`, `check`,
`build` and `solve` take the mapping in its place. There is no public
reader function. `axis_from` stays private, as `solve_over`'s reader and
the archive's.
- **A dict, not JSON or YAML.** `json.dumps(axis.to_dict())` is the JSON
form. The spec has `to_yaml` because its file is YAML; an axis lives in
`axis.json`.
- **The keys are the dataclass fields.** `axis_from` reads the keys an
axis takes and the ones it needs off `dataclasses.fields`, so a field
added to an axis is accepted and named in refusals with no second list
to update. `each` picks the class from one table, `_EACH`.
- **The carry is one nested key, `CARRY`.** Its parameters and variables
are keys inside it, and a dimension's name is always a value, so no name
in a spec can collide with the format: a dimension called `steps` and a
carry of `{'steps': ..., 'dim': ...}` read back equal.
- **Refusals name the keys.** An `each` that names no axis, a key the
axis does not take, and a key it needs and lacks each raise
`SpecsolveError` before a slice is taken. For example: `EachWindow given
as a mapping takes 'each' and ['dim', 'steps', 'lookahead', 'into',
'carry'], of which ['dim', 'steps', 'lookahead', 'into'] are required:
['lookahead'] is missing.`
- **No `carry` key where nothing is carried**, so an archive's
`axis.json` stays byte-identical and `INPUTS_LAYOUT` stays at 1. A test
holds the literal dict.
- `axis_manifest` is gone: the archive writes `axis.to_dict()`.

</details>

<details><summary>Mutation table, full suite</summary>

I took these by hand, because each is a substitution: committed tree,
`git checkout --` restore, `__pycache__` dropped on both sides, tree
checked clean after every run. The first four were taken on `ab3f613`.
The last was taken again on `8a558c0`, after `to_dict` was rewritten.

| mutation | result | tree |
|---|---|---|
| `axis_from`: no refusal of an `each` that names no axis | caught:
`…refused_with_the_keys_it_takes[no-each]`,
`[an-each-that-names-no-axis]` | clean |
| `axis_from`: unknown keys not looked for | caught:
`[a-key-the-axis-does-not-take]` | clean |
| `axis_from`: missing keys not looked for | caught:
`[a-key-the-axis-needs]` | clean |
| `solve_over`: the mapping not read as an axis | caught:
`test_solve_over_takes_an_axis_as_its_dict` and the four refusal cases |
clean |
| `to_dict`: the `carry` key written even when empty (`if self.carry` →
`if True`) | caught:
`test_an_axis_that_carries_nothing_writes_no_carry_key` | clean |

The last test was written for that mutation. The round-trip and archive
tests would not catch it, because both sides go through `to_dict`.

</details>

<details><summary>Gates, and what was not done</summary>

- **Gates**, on `8a558c0`. `pixi run check`: lint, format and typecheck
clean, 5621 passed, 566 skipped, 4 xfailed. `pixi run -e docs
docs-test`: 112 passed, so the new docstring links resolve. `pixi run -e
docs docs-build` passed, strict, on `ab3f613`; the later commit touches
no page.
- **Docs.** The sweeps reference ("The axes") says an axis is also plain
data and how a bad mapping is refused.
- **Not done.** `load_archive` and `load_inputs` still give the axis
back as an object, not a dict. #1888 adds a tuple of axes; when it
merges this in, `to_dict` and `axis_from` need a form for the tuple. The
`each` key and its two values are still string literals, in `to_dict`
and `_EACH`. The browser app's worker is not changed here.

</details>

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

https://claude.ai/code/session_01G1TmcQZZCctsVr54HE93qS

---------

Co-authored-by: Claude <noreply@anthropic.com>
…rt-e3g6om-windows

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_014wLrXpeSGDqoBFxyrs8NEz
@FBumann FBumann changed the title feat(api): a sweep can be cut by several axes at once, each carrying its own state, such as a rolling horizon per scenario feat(api): a sweep can be cut by several axes at once, each carrying its own state, such as a rolling horizon per scenario or a pathway whose decades roll through their hours Oct 12, 2026
…nce one axis writes the axis.json main writes

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

This branch has not been deployed

No deployments
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