Skip to content

feat(language): a piecewise block states its dims and names its links, and its where reaches links that walk a relation - #630

Open
FBumann wants to merge 36 commits into
mainfrom
claude/adoring-galileo-mu42ir
Open

FBumann wants to merge 36 commits into
mainfrom
claude/adoring-galileo-mu42ir

Conversation

@FBumann

@FBumann FBumann commented Sep 22, 2026 •

Copy link
Copy Markdown
Contributor

Prompt: "Review the PR about piecewise. Can you imagine a better or more capable api? #569" … "Do a stacked PR" … "It does mix curve and non curve converters!" … "Should we then combine 630 and 635?" — "Do it" … "Keep #569's design and re-port #638's load-time checks onto it, which also reverts the points: language." … "YOu can also close and redo the contents!" … "Lets try to simplify 630 as much as possible. Can we reuse existing things instead of adding new concepts, classes or methods?" … "sos: over → along rename is right / cone typesetting for a block with three or more links is also right" … "I think we should land the "along" change for sos on main independently!" … "Rebase 630" … "Rebase onto 661" … "Get #630 up to date" … "782 merged"

Note

The following content was generated by AI.

Based on main. #661's stack merged, and main is merged in up to #782.

What this changes

Every block states dims:. Links are keyed by name, and a link finer than the curve always walks a relation. The block's where: says which curves exist and how far each runs, and reaches walked links through their relation, so only converters with a curve get one. This PR carries #569, now closed in its favour, with main's points: reverted in favour of where:.

piecewise:
  operating_point:
    along: bp
    dims: [converter, snapshot] # required: one curve per coordinate of these
    where: has_curve # only some converters run on a curve
    links:
      rate: { expression: rate, values: bp_rate, by: converter_of, over: converter, into: flow } # row: operating_point_rate

The expansion writes this row:

operating_point_rate:
  dims: [flow, snapshot]
  where: at(has_curve, by=converter_of, over=converter, into=flow)
  expression: (rate) == sum(at(operating_point_lam, by=converter_of, over=converter, into=flow) * bp_rate, over=bp)

Breaking for the file language

  • piecewise: blocks say along: and where:; main's points: (a bool parameter naming how far each curve runs) is gone, since where: says it.
  • dims: is required on a block, and links: is a mapping: the emitted row is <block>_<link>.
  • A sign on a link among three or more is admitted, and prints as a cone; main refused it.

The sos: over → along rename landed on its own in #646 and is no longer part of this diff.

What the program carries

A program mirrors the spec, so a piecewise: block still in the spec is a typed curve under program.piecewise. Here that curve is PiecewiseDeclaration(along, links, method, frame, where, activity), where frame is dims: as written and where the typed mask, with ragged saying whether it reads along. Each Link carries its name, its typed expression, its values, its sign, the walk (by, over, into) and two facts lowering decides: dims, the row the link builds, and reads, whether that row reads the where through the relation. The expansion and the typesetter read those and compute nothing of their own.

Where a rule lives What it decides
validation.reference_errors (_piecewise_references, _piecewise_link_shape, _piecewise_walk_shape) along, dims:, each values parameter, a walk's relation and columns, and the gate exist as the block needs them
lowering.lower → piecewise.declaration_of each link's row; the expression carries exactly it; the values parameter varies along it and along only; the where tests dims: and along only; a walked row reads the where through its relation or not at all
validation.emitted_name_errors no link names a row the block writes for itself, and no emitted name is one the file declares
piecewise.assumptions_of(name, curve, where) what the method assumes, from the curve as lowered and the where as the file wrote it, since a typed mask has no text

What #638 and #643 decided at load, re-ported onto this design

Rule Where it lives here
a breakpoint parameter is float or int _piecewise_link_shape
an lp x-link carries a variable, or the domain rows decide nothing piecewise.lp_domain_refusal, on the named link
an undeclared along: is one line, not one per link _piecewise_references
a link, a gate or a where: outside the frame is refused on the block's line, never on an emitted row declaration_of, as the spec loads
PiecewiseExpansionError no longer exists a block is refused as a SchemaError or a DimensionError like any other declaration
an emitted name that collides names the block and the declaration emitted_name_errors, shared with sos:
Each change, and the finding it answers
Change Why
dims: required. A missing dims: is refused, and the message names the rewrite. The inferred frame, _frame, _widen and the second rewrite message go #569 says "the frame is something the file states", but it still inferred the frame where no link was refined. Variables and constraints already require dims:
The gate may not add dims to dims:. A gate over fewer dims switches every curve it covers activity: widened a declared dims: [snapshot] to [snapshot, generator] without a message
links: is a mapping. The emitted row is <block>_<key>, not _link<i>. A key that is not a name is refused, and so is a key whose row name the block already writes (convexity, lam, adjacency_below, …). Refusals name link 'power', not link 0 Positional names renamed constraints when links were reordered
The bare into: split is removed. A link row finer than dims: is a walk with by, over and into together; into: alone is refused into had two meanings; in at and sum it only names produced columns. No example or known model used the split
Walk checks run at load. over must reach a dim of dims:, into must not reach a dim of dims: or along, and a column may not repeat A walk into a dim of dims: was refused only by the emitted row, naming Constraint 'coupling_link0'. into: [flow, flow] loaded
A block where: reaches walked links. A walked row reads the mask through its relation, at(<where>, by, over, into), when the mask carries every dim the walk consumes or joins on. A ragged mask goes through as at(count(<where>, over=bp) > 0, …). A mask carrying none of them (e.g. snapshot only) reads as written. A mask carrying some and not the rest is refused, naming the missing dims. A walk into several columns reads it as at(…, into=[flow, effect]), which #782 lets a where string parse #569 refused a block where: beside a walk. So a model with some converters on a curve built weights and binaries for every converter
A walked link checks its own breakpoints. <block>_<link>_complete asks for its values only at the rows it reads the curve at: under the mask read through the relation, or where the relation has a row. <block>_complete keeps the links on dims:. The per-link name is reserved Asked with the other links, bp_rate was demanded at every flow, including flows of converters with no curve
Every derived assumption carries the where:. With a frame-only mask it goes on as written, or conjoined with the position test. With a ragged mask, the frame-level conditions take count(where, over=bp) > 0 The mask lived only on the program's curve record. After expand() → to_yaml() → load, _complete and _breakpoints were asked at generators with no curve
Typesetter: the quantifier is dims:. A walked link prints as a family, (rate_{f,t})_{f ∈ F : converter_of(f) = c}. A block with more than two links and a sign prints as (…) ∈ pwl(…) + {0} × ℝ≥0 × ℝ≤0, with three new operators: origin, nonnegative, nonpositive pinned = links[1 - bounded] assumed two links: q ≤ pwl(…)(q) dropped p and c. The coupling example printed ∀ t, g, f, putting every flow on every curve
Canonical form: a block's links are a mapping, so the list form's expression is normalised per named link, and a walk's mapping is left as it is. Link order stays as written main's canonical.py (#731) read links as a list, and indexed a walk's mapping with [0]
examples/piecewise_coupling.yaml is fluxopt's mixed system. A CHP on a curve sits beside a fixed-ratio boiler, and one has_curve is read both ways: the curve reads it, and fixed_ratio reads NOT at(has_curve, …) fluxopt mixes converters on a curve with converters at a fixed ratio
Docs: the reference page, reading.md, the examples, the golden model (chp_curve, and bus_curve masked), the schema and the generated pages The key table said "two or more links", and the refined-link table had two conflicting where: rows
History: the merges of main, the simplification, and the rebases

main at alpha.118 plus #626 through #643 was merged in, then alpha.119. Nine files conflicted, and each took main's side except the piecewise design. The thirteen tests #643 added for points:, list links and link 0 now speak where:, named links and link 'p'.

The simplification then folded what the branch had added beside a main counterpart back into it: Curve, check() and five module-level emitters into _Block; Names and ASSUMED into Emitted; CurveMask into the three mask helpers plus is_ragged; ResolvedPiecewise and Resolved.expanded_piecewise into one piecewise_where field; PiecewiseDeclaration.where, declaration_of(where) and the exists-mask resolution of written-out blocks are gone, since the assumptions carry the mask, so the program's curve record is main's.

After #646 merged, the branch was rebased onto main as one commit carrying the same tree: the 78 commits held two merges of main and would not replay one by one. After #654 merged, the commit was rebased again: the single-link and the four-element cases #654 added now write a mapping and meet the same messages, and the case refusing a sign among three links is dropped, since this PR admits it and the golden model's chp_curve covers it.

Onto #661's stack. That stack moved lowering into Spec.program, removed Resolved, and made the program carry a typed curve, so ten files conflicted and the piecewise design was re-ported onto its shapes rather than merged hunk by hunk. What moved: the reference rules from Spec's validators into validation.reference_errors, as the stack keeps every such rule; the fit rules from _Block.__init__ into declaration_of, which lowering calls to build the curve, so _Block only writes; Resolved.piecewise_where into PiecewiseDeclaration.where; is_ragged into PiecewiseDeclaration.ragged; walk_reads and through(reads=) into Link.reads; and the typesetter reads the program alone, as every other walk on the stack does. curve_frame, which the stack kept for main's design, is gone with the design.

Up to date with main at #780 (a merge commit, no force-push). main had renamed the package to mathspec and model.py to spec.py, renamed the test fixture override to varied, moved docstring links from Sphinx roles to [`name`][], and called a file a spec. Fourteen files conflicted. Each kept this branch's piecewise design and took main's names, links and wording around it. canonical.py and one case in test_canonical.py were taught the link mapping. docs/howto/curve-by-hand.md stays deleted, and the four curve pages sit beside main's component library in the examples index and nav. The PR's line is now under ## Upcoming version in CHANGELOG.md.

#782, and fluxopt's investment curve. A block where: beside a walk into: [flow, effect] failed the load on the assertion that what a method assumes is stated in the language: the where grammar could not parse the list the walk reads the mask through (#781). #782 fixed the grammar on main, and a second merge brings it in, with CHANGELOG.md the only conflict. test_a_block_mask_reaches_a_walk_into_several_columns loads fluxopt's investment cost curve (fluxopt/fluxopt#26), checks the where: on its cost row and on its breakpoint check, and loads the expansion again. With _where_parser.py from before #782 it fails on that assertion.

The generated files were regenerated after each rebase and merge.

Gates, and what was not run

On the head 21e31bd, in a uv venv (Python 3.12) with the pinned tool versions, since pixi cannot be installed here:

Gate Result
pytest -q -n auto 2696 passed, 2 skipped, the docs tests included
ruff check, ruff format --check (0.16.1) clean
pyrefly check (1.2.0) 0 errors
prettier --check on CHANGELOG.md, typos clean
schema, golden output, generated pages regenerated; no diff
zensical build --strict (0.0.63) run on d97fb41, not rerun since: one issue, pathlib.Path unresolved, because the proxy refuses docs.python.org's inventory
compile-tex not run: no tectonic here
Decided, and deliberately not done

Why

It follows the review of #569: only the block decides the curves, a link that fans out walks a relation as at does, and rows are named rather than numbered. fluxopt mixes converters on a curve with converters at a fixed ratio, one to five flows each, and examples/piecewise_coupling.yaml now holds that system. main had meanwhile said how far a curve runs with points:; this design says it with where:, and the choice went to where:.

🤖 Generated with Claude Code

https://claude.ai/code/session_01HxoXSQCyrcDs6vuRk3x6tz

https://claude.ai/code/session_015h57WkBDnpxrknuJ5zZy9F

https://claude.ai/code/session_015VUkQfE5dT7mEGoTQXSxN3

@FBumann
FBumann added this pull request to stack #636 September 22, 2026 20:47
@FBumann FBumann changed the title feat(language): a piecewise block states its dims and names its links, and a finer link walks a relation feat(language): a piecewise block states its dims and names its links, and its where reaches links that walk a relation Sep 23, 2026
@FBumann
FBumann removed this pull request from stack #636 September 23, 2026 09:01
@FBumann
FBumann added this pull request to stack #642 September 23, 2026 09:01
@FBumann FBumann added the area: formulations Blocks expanding to declarations: piecewise, indicator, McCormick label Sep 23, 2026
@FBumann FBumann added this to the Conditional piecewise curves milestone Sep 23, 2026
@FBumann
FBumann removed this pull request from stack #642 September 23, 2026 12:07
@FBumann
FBumann changed the base branch from claude/mathspec-piecewise-api-7j0wpt to main September 23, 2026 12:07
@FBumann
FBumann force-pushed the claude/adoring-galileo-mu42ir branch from 3e21e13 to cb8dc0c Compare September 23, 2026 13:31
… and every curve is written out before a model becomes one

Resolved is gone. Lowering builds the Program as a model loads and holds
it on the Spec; to_program(spec) returns that object, and returns the
expansion's for a model with a curve, so a program holds the rows a curve
states and never the curve. Named joins program.Expression, so a use of an
expressions: entry stands where it is read and the typesetter prints from
the program beside the file. ExpressionDeclaration carries its dims, and
piecewise.curve reads a block's typed links and frame off the model for
the expansion and the walk. PiecewiseDeclaration, Program.piecewise,
Spec.resolved, lowering.inline and the refusal of a model still carrying
a curve are removed.

The typesetter's output is unchanged: the golden files and the generated
pages regenerate byte for byte.

Docs sentences, after (before): reading.md n 75 avg 16.1 median 15
over25 10 (72, 16.4, 14, 13); piecewise.md n 70 avg 17.6 median 15
over25 13 (69, 17.8, 15, 14).

Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01TrvhjFQCQJ6ATBQkfhcoMi
…s included, and the typesetter reads it alone

to_program lowers the model as it arrived: a piecewise: block still in it
is a curve on the program, typed, with its links, signs, method, gate, mask
and frame, and the program of spec.expand('piecewise') carries the rows
instead. Every declaration carries its description, and the program the
file's. The typesetter takes a Program and reads nothing else; the walk no
longer re-resolves a curve's links at print time, and piecewise.curve is
gone. relations_of moves from Spec to Program. advice() writes curves out
itself and refuses a program still carrying one.

The typeset output is unchanged: the golden files and the generated pages
regenerate byte for byte.

Docs sentences, after (before): reading.md n 75 avg 16.8 median 15
over25 12 (75, 16.1, 15, 10); piecewise.md n 70 avg 17.6 median 15
over25 14 (70, 17.6, 15, 13).

Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01TrvhjFQCQJ6ATBQkfhcoMi
… lowering, before any expression is read

The nine after-validators on Spec that read one declaration against the
others — name collisions, frames over declared dimensions, relation
targets, bound names, set shapes and bounds, curve references and the names
an expansion would collide with — are functions in validation.py, collected
by reference_errors and run first by lowering.lower. Spec keeps the shape
rules pydantic decides per block, and no longer imports sos or the operator
table; side_columns is the public name of the relation helper the rules
share. Every message is the same string, and every refusal the same class.

Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01TrvhjFQCQJ6ATBQkfhcoMi
… is advised as its rows are

The guard landed in the previous commit without the test that fails without
it. With the guard deleted, the new test fails: a program with a block is
advised on the file's own rows as if the curve stated none.

Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01TrvhjFQCQJ6ATBQkfhcoMi
… the program owns the vocabulary the file declares in

Loading no longer expands every curve to validate the expansion: the rows a
curve states are held to the language when expand() writes them out, since
an expansion is a model like any other. A model with a curve is validated
once at load rather than twice.

The dtype, domain, absence, sense, set-order and method vocabulary moves
from model.py into program.py, which model.py imports. program.py no longer
imports model.py, so the type-only import of Program on Spec and its noqa
go, and sos.py imports Spec plainly instead of inside a function.

Load cost, before (after): examples/piecewise.yaml validated 2 (1) times
and parsed 9 (4) expressions; examples/sos.yaml the same.

Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01TrvhjFQCQJ6ATBQkfhcoMi
…he one door to both states is to_spec

to_program is gone. Spec.program is the model typed, section for section,
built once as the model loads; spec.expand(...).program is its rows. A
consumer building rows reads the sections it takes and refuses a curve or a
set it finds, the way it already had to for a set; advice() does so for a
Program handed to it, and writes curves out itself for a file or a Spec.
typeset and typeset_declaration take a Program as before.

Docs sentences, after (before): reading.md n 75 avg 17.1 median 15
over25 14 (76, 16.8, 15, 12); piecewise.md n 70 avg 17.7 median 15
over25 13 (70, 17.6, 15, 14); what-counts-as-public-api.md n 18 avg 16.0
median 14 over25 3 (unchanged count); limits.md n 55 avg 18.3 median 19
over25 14 (unchanged count).

Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01TrvhjFQCQJ6ATBQkfhcoMi
…the model loads

The private attribute lowering filled and the property asserting over it
are one cached_property computing lower(self); the after-validator forces
it, so a Spec in hand has still passed the whole language.

Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01TrvhjFQCQJ6ATBQkfhcoMi
@FBumann
FBumann force-pushed the claude/adoring-galileo-mu42ir branch from cb8dc0c to 9e687c6 Compare September 23, 2026 16:20
…ules beside the namespace

_Resolver, one class over both grammars, is ExpressionResolver in
_expression_resolver.py and WhereResolver in _where_resolver.py, the where
walk building a side that is an expression through the expression walk.
resolution.py keeps the Namespace and the doors lowering calls. The three
methods the where walk reads from the expression walk are public on it;
names_in and the literal-number helper move to the parser module beside
the other helpers over parsed nodes. Every message is the same string.

Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01TrvhjFQCQJ6ATBQkfhcoMi
…he program carries, and nothing else

The Curve alias, a tacit protocol between the pydantic block and the
program declaration, is gone: assumptions_of, Emitted.of and the curvature
rule take a PiecewiseDeclaration. The expansion keeps the block for the
link text its rows repeat and takes the declaration for the frame and the
names it writes. The two emitted-name collision rules read the program
rather than the file, so they run once the declarations exist; every
message is the same string.

Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01TrvhjFQCQJ6ATBQkfhcoMi
…s use off the program, and the walk keeps no record

The glossary and the three kinds of note move from walk.py to legend.py.
What they explain is read off the program before anything prints, by
notice(), so the walk no longer fills a Noticed record as it prints and a
subscript no longer mutates the walk through its context. Walk.line refuses
a name declared as none of the five kinds or as two, so typeset_declaration
is one call. Symbols is a frozen record built by symbols_for. The typeset
output is unchanged: the golden files match byte for byte.

Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01TrvhjFQCQJ6ATBQkfhcoMi
@FBumann
FBumann changed the base branch from main to claude/confident-faraday-coc7mj-11 September 23, 2026 22:04
The unboundedness pass read a set as it is, every variable it restricts
being named by a row, and refused a curve. A curve states its rows the
same way: each link names the variables a link row would, so the pass
reads the links and the answer is the expansion's with nothing expanded.
The refusal goes, and check loses --expand, a flag that would change no
answer.

Guard: with the line reading the links deleted, three tests fail: the
advice test over the file, the Spec and the Program; the boundedness
case carried-by-a-curve; and the existing test that a curve holds its
variables, which now runs on the block.

Docs sentences, after (before): check.md n 20 avg 12.8 median 14 over25
1 (21, 13.3, 14, 2); what-counts-as-public-api.md n 20 avg 18.7 median
15 over25 4 (20, 18.1, 15, 4).

Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01TrvhjFQCQJ6ATBQkfhcoMi
@FabianHofmann

Copy link
Copy Markdown
Contributor

@FBumann I am seeing this PR and its where implemention for piecesewise as required for the completion of the pypsa yaml. So I guess we can focus on bringing in piecewise after the refactoring again.

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

main landed the stack this branch was built on as one squash commit per
PR. The squash of #661 has the same tree as 279997a, the stack head
this branch contains, so this merge records it and changes no file. The
merge of main that follows then brings only what landed after it.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01GpnFcQDuvt2bExGngb6MYk
After the head of #661 and the recorded squash, main brought #666, #668
and the release. This branch rewrote piecewise.py around declaration_of
and named links, so it keeps its own file and takes #666's four changes
to it: the module docstring's first sentence, Emitted.written, the
leaves_ungated rule and its use in the gate rows. PiecewiseDeclaration
keeps this branch's attributes and gains main's description line. The
reading page keeps this branch's link row names and main's
spec.expand() == spec.expand(). main dropped the duplicate boundedness
test, and it stays dropped.

Tests #661 and #666 added in the old block syntax now write along:,
dims: and links by name: carried-by-a-curve, the curve in the
resolve-once test, which also resolves the curve's where once, and the
symbol-table test, whose link rows are named after their links.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01GpnFcQDuvt2bExGngb6MYk
FBumann and others added 2 commits September 24, 2026 16:02
…d as the model loads (#685)

A walked link is read as at() reads its relation, so into must land on the
key, the read must be single-valued, and every joined key column must be a
dim of dims:. An empty over or into is refused. No two blocks may write the
same name. A where over a dim the walk only joins on reads as written. The
missing-breakpoint advice and the where-over-a-walked-dim refusal name a
rewrite the block can take.


Claude-Session: https://claude.ai/code/session_01WasiCKk1SebD7S1CsAFQog

Co-authored-by: Claude <noreply@anthropic.com>
main renamed the package to mathspec and model.py to spec.py, renamed the
test fixture override to varied, moved docstring links to [`name`][], and
added canonical.py. The piecewise design keeps this branch's side; each
conflict takes main's names, links and wording around it.

- canonical: a block's links are a mapping, so the list-form expression
  is normalised per named link and a walk's mapping is left as it is.
- test_canonical: the reversed-links case reverses the mapping.
- curve-by-hand.md stays deleted; the curve pages sit beside the
  component library in the examples index and nav.
- "model" reads "spec" where the added prose means the file.
- CHANGELOG: the PR's line under Upcoming version.

Schema, golden output and generated pages regenerated.

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

FBumann commented Sep 29, 2026

Copy link
Copy Markdown
Contributor Author

Prompt: "Discuss hs request with me" … "Do all 3"

Note

The following content was generated by AI.

Feedback from porting fluxopt: a walked activity: gate is accepted as a follow-up PR on this one. The where: crash is a main grammar gap, filed as #781. A link that adds a dimension waits for fluxopt/fluxopt#26.

Each ask, checked against the code

1. activity: reads its binary through a relation. Accepted, as a stacked follow-up.

A unit-commitment converter's on/off binary is running[status_entity, time, period]. The curve is on converter, and pw_status_of maps one to the other. _piecewise_references admits only a declared binary variable whose dims all lie in dims:, so fluxopt declares a copy, pw_on, and a constraint that only re-indexes it. The ask:

activity: {variable: running, by: pw_status_of, over: status_entity, into: converter}
  • It uses the same keys and load checks a walked link already has.
  • A converter with no row in pw_status_of stays ungated. The second gate row needs where: NOT at(running, by=pw_status_of, over=status_entity, into=converter), and that where: loads on this head today.
  • It goes in its own PR on top of this one: one issue, one PR, and the workaround is correct in the meantime.

2. A block where: beside a walk into: [flow, effect] fails with an internal assertion. The cause is on main: #781.

Reproduced on this head: the block loads without where:, and with where: has_cost_curve the load fails with SchemaError: Assertion failed, what a method assumes is stated in the language. The mask is read through the walk as at(has_cost_curve, by=cost_of, over=cost_curve, into=[flow, effect]). On main, a plain constraint with that where: fails to parse too, and into=flow loads. Once #781 lands, this PR takes main and adds the block as a regression test. A refusal of its own goes in here only if this PR merges first.

3. A curve per flow whose cost link adds effect. Not on this PR.

The shape is dims: [flow, period] with a link over [flow, effect, period]. The loader cannot tell it from a link that carries time on a curve without time, which pins one operating point across every step. The ask is a walk whose over: consumes nothing, through a relation keyed [flow, effect]. That changes the language, and nothing needs it until fluxopt/fluxopt#26 is scheduled. The workaround today is a curve on its own cost_curve dimension, with two relations.


Generated by Claude Code

Takes #782, which lets a where string name several columns in at's
over= and into=. CHANGELOG keeps both lines.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_015VUkQfE5dT7mEGoTQXSxN3
fluxopt's investment cost curve reads each effect of a sized flow off
its curve, so its mask reaches the cost row as at(…, into=[flow, effect]).
Without #782 the load fails on the assertion that what a method assumes
is stated in the language.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_015VUkQfE5dT7mEGoTQXSxN3
FBumann pushed a commit that referenced this pull request Sep 29, 2026
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_015VUkQfE5dT7mEGoTQXSxN3
@FBumann
FBumann added this pull request to stack #785 September 29, 2026 09:29
Takes #788. CHANGELOG keeps both lines.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_015VUkQfE5dT7mEGoTQXSxN3
FBumann pushed a commit that referenced this pull request Sep 29, 2026
Takes #788 through #630.

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

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

area: formulations Blocks expanding to declarations: piecewise, indicator, McCormick

Projects

None yet

Development

Successfully merging this pull request may close these issues.

3 participants