Repository navigation
Conversation
…orted by name, whatever order a file writes A declared dims: now reaches the program sorted, as a derived frame already did: parameters, variables, constraints, given entries, a named expression's declared dims, and the conditions that read them. The typesetter orders a sum's dummies, a dual's indices and an assumption's frame by name instead of by the dimensions: section. The canonical form sorts every dims: list, and merge no longer refuses readers that list a sum's dims in different orders, since the order no longer reaches anything. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01D2S9sN1jLGGMCCwqwVyGD6
…tates its dims sorted Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01D2S9sN1jLGGMCCwqwVyGD6
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01D2S9sN1jLGGMCCwqwVyGD6
`_frame_of` keeps #899's body dims and sorts a declared `dims:` with #899's `sorted_dims`. Every program dims this branch sorts goes through `sorted_dims` too, so the rule has one home. A relation's key dims keep `tuple(sorted(...))`, because a key may name one dimension twice and the refusal of a bare relation reads the repeat. The reading guide says an entry's dims are sorted, as a node's are. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01NNjZXCjH1FpCvxUvDGjWNw
A named expression's dims are its body's dims: dims: gives only dims the body is indexed over (#898), the body is indexed only over dims dims: gives, and both are sorted by name. So `_frame_of` goes. The leaf dims the namespace holds and a constraint's dims are already sorted, so reading them sorts nothing again. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01NNjZXCjH1FpCvxUvDGjWNw
… other dims is `Mask.dims` was the one `dims` in the program that was a frozenset. It is now sorted by name, so a named condition's frame, a translated condition's dims and the typesetter's assumption frame read it as it is. The three places that took a union or a difference build a set first. The test that every program dims is sorted asks each mask for its dims, since `Mask.dims` is a property rather than a field. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01NNjZXCjH1FpCvxUvDGjWNw
…ition, not the mask around it The walk now also yields `Mask.dims`, so the test starts at the leaf it asserts on. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01NNjZXCjH1FpCvxUvDGjWNw
…orted by name The order a file writes a relation's `key:` and `values:` in now reaches nothing, as #914 does for `dims:`. `Relation.columns` holds the key's columns first, each side sorted by role, and a join's columns follow that order (`Relation.in_order`). The legend prints the table in it, and the canonical form sorts each side. `Relation.dims` was the dimension of each column, in column order and with a repeat where two columns share one. It is now every dimension a column is over, once, sorted by name, as every other `dims` is. The test that every program dims is sorted asks each relation for it, and the tests that reverse every `dims:` list reverse each relation's columns too. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01NNjZXCjH1FpCvxUvDGjWNw
FBumann
added this pull request to stack #917
October 20, 2026 19:40
…s:` still changes The order of the entries under `dimensions:` changes nothing in the program, but the typeset document lists the dimensions in that order, and the dimension declared first takes a shared first letter as its index. The sum's docstring and the golden caption no longer say "file order". Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01JzRHmodLpxhaZRQc9sgBdZ
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01JzRHmodLpxhaZRQc9sgBdZ
This was referenced Oct 21, 2026
Contributor
Author
Note The following content was generated by AI. Superseded by #924, which squashes #914, #916 and #921 into one commit on Generated by Claude Code |
FBumann
removed this pull request from stack #917
October 21, 2026 07:37
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
Note
The following content was generated by AI.
What this changes
Every
dimsandframein the program is sorted by name, including an entry's declareddims:. A relation's columns are its key's first, each side sorted by name. Indices and tables print in that order, and the canonical form sorts everydims:list and each side of a relation. #911 did this for derived dims. This PR makes the rule universal.The branch merges
mainwith #899, so the expression nodes'dimsfollow the same rule. All sorting of program dims goes through #899'ssorted_dims.Mask.dimsandRelation.dimsare sorted tuples too, so everydimsin the program has one type. The code that sorting made redundant is gone.What moves, and what a reader sees
dims:writesdims:dims:writesParameterDefined,VariableDefined,ParameterComparison)RelationDefined,RelationComparison,RelationPairComparison)Relation.columns,Relation.keykey:andvalues:JoinColumns.joined,.groupedRelation.columnsRelation.dimsMask.dimsfrozensetdimensions:, or of the filedims:list and a relation's sides as writtenmerge, where no file defines a sumWhat a reader sees:
p_{g,t}instead ofp_{t,g}.\lambda_{b,g,t}, with the breakpoint index first.injection_(b,t) = generator_injection_(b,t) + …[generator, bus]prints asconnection ⊆ B × G. A relation whose columns share a dimension prints in role order, soGenerator_maintenance_cover, keyed{scenario, generator, start, covered}, prints asT × G × Ξ × T.relation.columns, which are now in the new order.dims:sets the order of the indices are corrected.docs/first-spec.md,docs/reference/typeset.md, thenamed.mdexample anddocs/several-files.mdshow printed output and are edited by hand.The merge with #899, and what it made redundant
_frame_ofin_lowering.pyis removed. A named expression'sdimsare its body'sdims. Two rules make the sets equal:dims:gives only dims the body is indexed over (fix(language)!: a named expression whosedims:gives a dimension its expression is not indexed over is refused at load #898), and the body is indexed only over dims thatdims:gives. Both are sorted, so the tuples are equal.tuple(sorted(...))this branch added, and 5 thatmainhad, now callsorted_dims, apart from 3 that read a relation's key. Those keeptuple(sorted(...)), because a key may name one dimension twice, and the refusal of an ambiguous bare relation reads the repeat.test_a_rule_decided_without_data[where-bare-name-of-a-relation-with-two-columns-over-one-dim]fails whensorted_dimstakes those too.Namespace.leaf_dimsis sorted when it is built, so a leaf reads it as it is (_expression_resolver.py,_bound). The typesetter's dual reads the constraint'sdimswithoutsorted().Mask.dimsis a tuple sorted by name, not afrozenset. A named condition's frame, aTranslatedCondition's dims, two refusal messages and the typesetter's assumption frame read it as it is. The three places that took a union or a difference build a set first.program.pysorts inline, because it cannot import_dimensions.py, which imports it.docs/reference/reading.mdshowed an entry'sdimsas written. It now says that an entry'sdimsare sorted, as a node's are, and that a mask's.dimsare sorted.CHANGELOG.md(both lines kept),_lowering.py(_frame_of, since removed) and the imports oftests/test_dimensions.py(both sets kept).#899's
test_every_expression_node_states_its_dims_sorted_by_nameand this PR's_dims_tupleswalker both check the nodedimsfrom #899, because the walker reads every dataclass field nameddims. The walker also asks eachMaskand eachRelationfor itsdims, which are properties.Relations
Relation.columnsholds the key's columns first, then the values', each side sorted by role. The key stays in front because a relation reads as a map from its key to its values.Relation.keyis sorted by role.JoinColumns.joinedand.groupedfollow the same order, throughRelation.in_order. The typesetter prints a grouped sum's indices in it.Relation.dimsis every dimension a column is over, once, sorted by name. Its two callers took a set of it already.key:andvalues:of each relation, a list or a mapping.dims_reversedreverses each relation'skeyandvaluesas well, so the tests that a file's order reaches neither the program, the printed document nor the canonical text cover relations. ExpectedJoinColumnsintest_a_relation_lowers_with_the_join_each_call_namesare in the new order.The order is by role, not by dimension, because two columns may share a dimension and only the role tells them apart.
Why
A file's order now reaches nothing a consumer reads, so one rule holds everywhere. Two files that differ only in a
dims:list or a relation's column order state the same spec and write the same canonical text. A merge has nothing to refuse. If a custom index order is ever needed, it can be added later as one declared order for the whole spec.Verification
Tests.
test_the_order_a_file_writes_dimensions_in_reaches_no_value_of_the_programreversesdimensions:, everydims:list and each relation's columns in every example, and asserts an equal program, before and afterexpand().test_every_dims_of_the_program_is_sorted_by_namewalks every program and its expansion, and checks eachdimsandframe, a mask's and a relation's included.test_the_order_a_dims_list_is_written_in_prints_nowhereasserts that every example prints the same in all three formats with everydims:list and each relation's columns reversed.test_a_condition_on_a_relation_states_its_key_dims_sorted_by_nameuses a relation keyed[snapshot, generator]and reads it three ways.test_a_file_written_backwards_writes_the_same_textnow also reverses everydims:list and each relation's columns.Gates on
cc88eb2:pixi run lintpasses.pixi run testpasses: 3269 passed, 1 skipped.pixi run docs-buildpasses.compile-texwas not run, because the container cannot download the TeX bundle. CI runs it.53b42fbalone fails 3 cases oftest_a_condition_on_a_relation_states_its_key_dims_sorted_by_name, because the walker then also yields the mask around the leaf.f328aaestarts that test at the leaf.Mutation table. Each change was made by hand and the full suite was run. The file was restored with
git checkout --,__pycache__was dropped before and after each run, and the tree came back clean each time. Rows 1 to 13 were run before the merge with #899; rows 1 to 9 on the commit before the relation test was added, rows 10 to 13 with it. Row 14 was run on53b42fb, and rows 15 to 20 oncc88eb2.dimensions:test_linecasesdims:listtest_a_file_written_backwards_writes_the_same_textandtest_the_fragments_merge_to_the_one_filetest_the_composed_sum_is_read_over_the_readers_frame[a-column-against-a-label][the-bare-relation][a-column-against-another-relation]Mask.dimssorted in reversetest_every_dims_of_the_program_is_sorted_by_nametest_the_order_a_file_writes_dimensions_in_reaches_no_value_of_the_program[pypsa.yaml]test_a_relation_is_declared_as_the_file_declares_ittest_a_relation_lowers_with_the_join_each_call_namestest_a_value_column_the_call_consumes_is_a_condition_like_a_produced_oneRelation.dimsone per column, in column ordertest_every_dims_of_the_program_is_sorted_by_name[operators/at.yaml]test_a_file_written_backwards_writes_the_same_textCoverage moved.
test_readers_that_order_the_frame_apart_are_refusedis nowtest_readers_that_list_the_frame_in_two_orders_compose_one_sum. It checks that both orders give one canonical text.the-dims-of-an-entrycase oftest_an_order_the_form_keeps_is_a_difference_in_the_textis removed, because the form now sorts that order.test_the_order_of_dimensions_reaches_no_value_of_the_programfrom fix(language): two files that declare their dimensions in different orders load to the same program #911 is widened to reverse everydims:list and each relation's columns as well.Not done
dims:list and each relation's columns as the file writes them, andto_yaml()writes them back that way, because aSpecis the author's file. Only the program, the typesetter and the canonical form sort.JoinColumns.joined_dimsand the other per-column*_dimsstay one dimension per column, paired with the roles by position. They are not sets of dims.scope.in_declaration_order, and readregion.when.dimsas it is.Generated by Claude Code