From 84a184613e42bb3cd74a8ec1db2b53602d7a598b Mon Sep 17 00:00:00 2001 From: Claude Date: Wed, 30 Sep 2026 09:09:09 +0000 Subject: [PATCH 1/8] feat(language): a construct that reads order needs an ordered dimension A dimension declares `ordered: true` where the order of its members is part of the model; the default is false. shift, sum_back, position(), a piecewise curve's `over:` and a type-2 set's `along:` read that order, and the loader refuses each along a dimension that is not ordered, naming the declaration to add. `ordered` is written only where true. Every example, test spec and docs block that steps along a dimension now declares it ordered; the generated pages, the schema and the golden output are regenerated. Co-Authored-By: Claude Opus 5.5 Claude-Session: https://claude.ai/code/session_01WQi3Nh7wYqbniKdGm8MrGF --- docs/examples/commitment.md | 2 +- docs/examples/operators.md | 18 ++--- docs/examples/pypsa/carrier.md | 1 + docs/examples/pypsa/generator.md | 2 + docs/examples/pypsa/generator_commitment.md | 2 + docs/examples/pypsa/generator_maintenance.md | 1 + docs/examples/pypsa/generator_ramping.md | 2 + docs/examples/pypsa/line.md | 2 + docs/examples/pypsa/link.md | 2 + docs/examples/pypsa/link_commitment.md | 2 + docs/examples/pypsa/link_maintenance.md | 1 + docs/examples/pypsa/link_ramping.md | 2 + docs/examples/pypsa/load.md | 1 + docs/examples/pypsa/network.md | 1 + docs/examples/pypsa/power_flow.md | 1 + docs/examples/pypsa/process.md | 2 + docs/examples/pypsa/process_commitment.md | 2 + docs/examples/pypsa/process_maintenance.md | 1 + docs/examples/pypsa/process_ramping.md | 2 + docs/examples/pypsa/security.md | 1 + docs/examples/pypsa/settings.md | 2 + docs/examples/pypsa/storage_unit.md | 2 + docs/examples/pypsa/store.md | 2 + docs/examples/pypsa/transformer.md | 1 + docs/howto/compose.md | 2 +- docs/howto/see-an-expansion.md | 6 +- docs/reference/language/assumptions.md | 2 +- docs/reference/language/dimensions.md | 41 ++++++++-- docs/reference/language/expressions.md | 6 +- docs/reference/language/operators.md | 11 +-- docs/reference/language/piecewise.md | 6 +- docs/reference/notation.md | 6 +- docs/reference/reading.md | 2 +- examples/commitment.yaml | 2 +- examples/operators/shift.yaml | 2 +- examples/operators/shift_by_parameter.yaml | 2 +- examples/operators/shift_edge.yaml | 2 +- examples/operators/shift_partitioned.yaml | 2 +- examples/operators/shift_wrap.yaml | 2 +- examples/operators/sum_back.yaml | 2 +- examples/operators/sum_back_by_parameter.yaml | 2 +- examples/operators/sum_back_partitioned.yaml | 2 +- examples/operators/sum_back_wrap.yaml | 2 +- examples/piecewise.yaml | 1 + examples/piecewise_lp.yaml | 1 + examples/piecewise_ragged.yaml | 1 + examples/ports/transport_pwl.yaml | 1 + examples/pypsa.yaml | 2 + examples/pypsa/carrier.yaml | 1 + examples/pypsa/generator.yaml | 2 + examples/pypsa/generator_commitment.yaml | 2 + examples/pypsa/generator_maintenance.yaml | 1 + examples/pypsa/generator_ramping.yaml | 2 + examples/pypsa/line.yaml | 2 + examples/pypsa/link.yaml | 2 + examples/pypsa/link_commitment.yaml | 2 + examples/pypsa/link_maintenance.yaml | 1 + examples/pypsa/link_ramping.yaml | 2 + examples/pypsa/load.yaml | 1 + examples/pypsa/network.yaml | 1 + examples/pypsa/power_flow.yaml | 1 + examples/pypsa/process.yaml | 2 + examples/pypsa/process_commitment.yaml | 2 + examples/pypsa/process_maintenance.yaml | 1 + examples/pypsa/process_ramping.yaml | 2 + examples/pypsa/security.yaml | 1 + examples/pypsa/settings.yaml | 2 + examples/pypsa/storage_unit.yaml | 2 + examples/pypsa/store.yaml | 2 + examples/pypsa/transformer.yaml | 1 + examples/pypsa_linearized_uc.yaml | 1 + examples/sos.yaml | 1 + schema/mathspec.schema.json | 7 +- src/mathspec/_expression_resolver.py | 5 +- src/mathspec/_where_resolver.py | 8 +- src/mathspec/errors.py | 9 +++ src/mathspec/resolution.py | 10 +++ src/mathspec/spec.py | 14 +++- src/mathspec/validation.py | 6 +- tests/expand/curve-activity/after.yaml | 2 +- tests/expand/curve-activity/before.yaml | 2 +- .../expand/curve-adjacency-points/after.yaml | 2 +- .../expand/curve-adjacency-points/before.yaml | 2 +- tests/expand/curve-adjacency/after.yaml | 2 +- tests/expand/curve-adjacency/before.yaml | 2 +- tests/expand/curve-convex/after.yaml | 2 +- tests/expand/curve-convex/before.yaml | 2 +- tests/expand/curve-lp-points/after.yaml | 2 +- tests/expand/curve-lp-points/before.yaml | 2 +- tests/expand/curve-lp/after.yaml | 2 +- tests/expand/curve-lp/before.yaml | 2 +- tests/expand/curve-sos2-piecewise/after.yaml | 2 +- tests/expand/curve-sos2-piecewise/before.yaml | 2 +- tests/expand/curve-sos2/after.yaml | 2 +- tests/expand/curve-sos2/before.yaml | 2 +- tests/expand/set-type2/after.yaml | 2 +- tests/expand/set-type2/before.yaml | 2 +- tests/fixtures.py | 4 +- tests/fixtures/every_program_node.yaml | 2 +- tests/test_advice.py | 2 +- tests/test_boundedness.py | 2 +- tests/test_canonical.py | 10 ++- tests/test_composition.py | 2 +- tests/test_dimensions.py | 6 +- tests/test_exclusivity.py | 4 +- tests/test_lowering.py | 18 ++--- tests/test_piecewise.py | 4 +- tests/test_separability.py | 7 +- tests/test_sos.py | 2 +- tests/test_validation.py | 81 ++++++++++++++++++- tests/typesetting/golden/model.yaml | 6 +- tests/typesetting/test_symbols.py | 2 +- tests/typesetting/test_walk.py | 34 ++++---- 113 files changed, 362 insertions(+), 119 deletions(-) diff --git a/docs/examples/commitment.md b/docs/examples/commitment.md index 43ccc2c6a..eb880a622 100644 --- a/docs/examples/commitment.md +++ b/docs/examples/commitment.md @@ -22,7 +22,7 @@ description: >- inequality is written once. dimensions: - snapshot: { dtype: int, description: dispatch periods } + snapshot: { dtype: int, description: dispatch periods, ordered: true } generator: { description: generating units } parameters: diff --git a/docs/examples/operators.md b/docs/examples/operators.md index a146fa522..2cfd46079 100644 --- a/docs/examples/operators.md +++ b/docs/examples/operators.md @@ -287,7 +287,7 @@ description: >- it would have fed is not built. dimensions: - snapshot: { dtype: int } + snapshot: { dtype: int, ordered: true } variables: p: @@ -314,7 +314,7 @@ description: >- reads the last and nothing is vacated. dimensions: - snapshot: { dtype: int } + snapshot: { dtype: int, ordered: true } variables: p: @@ -341,7 +341,7 @@ description: >- number instead of being absent, so the row survives. dimensions: - snapshot: { dtype: int } + snapshot: { dtype: int, ordered: true } variables: p: @@ -370,7 +370,7 @@ description: >- dimensions: technology: { dtype: str } - month: { dtype: int } + month: { dtype: int, ordered: true } parameters: lead: { dims: [technology], dtype: int } @@ -401,7 +401,7 @@ description: >- snapshot reads that season's last and no level crosses the boundary. dimensions: - snapshot: { dtype: int } + snapshot: { dtype: int, ordered: true } season: { dtype: str } relations: @@ -433,7 +433,7 @@ description: >- dimensions: unit: { dtype: str } - hour: { dtype: int } + hour: { dtype: int, ordered: true } parameters: min_up: { dims: [unit], dtype: int } @@ -467,7 +467,7 @@ description: >- dimensions: unit: { dtype: str } - hour: { dtype: int } + hour: { dtype: int, ordered: true } parameters: min_up: { dims: [unit], dtype: int } @@ -501,7 +501,7 @@ description: >- dimensions: unit: { dtype: str } - hour: { dtype: int } + hour: { dtype: int, ordered: true } parameters: min_up: { dims: [unit], dtype: int } @@ -536,7 +536,7 @@ description: >- dimensions: unit: { dtype: str } - hour: { dtype: int } + hour: { dtype: int, ordered: true } day: { dtype: str } relations: diff --git a/docs/examples/pypsa/carrier.md b/docs/examples/pypsa/carrier.md index aaa4e5623..fdf8ba223 100644 --- a/docs/examples/pypsa/carrier.md +++ b/docs/examples/pypsa/carrier.md @@ -13,6 +13,7 @@ dimensions: period: description: investment periods — PyPSA's `investment_periods` dtype: int + ordered: true carrier: description: energy carriers, what a growth limit is set per diff --git a/docs/examples/pypsa/generator.md b/docs/examples/pypsa/generator.md index 3461fcd08..091d43c65 100644 --- a/docs/examples/pypsa/generator.md +++ b/docs/examples/pypsa/generator.md @@ -15,6 +15,7 @@ dimensions: snapshot: description: dispatch periods dtype: datetime + ordered: true bus: description: network nodes generator: @@ -24,6 +25,7 @@ dimensions: period: description: investment periods — PyPSA's `investment_periods` dtype: int + ordered: true carrier: description: energy carriers, what a growth limit is set per diff --git a/docs/examples/pypsa/generator_commitment.md b/docs/examples/pypsa/generator_commitment.md index d793efc36..c50d19751 100644 --- a/docs/examples/pypsa/generator_commitment.md +++ b/docs/examples/pypsa/generator_commitment.md @@ -15,11 +15,13 @@ dimensions: snapshot: description: dispatch periods dtype: datetime + ordered: true generator: description: generating units, each on one bus period: description: investment periods — PyPSA's `investment_periods` dtype: int + ordered: true relations: snapshot_period: diff --git a/docs/examples/pypsa/generator_maintenance.md b/docs/examples/pypsa/generator_maintenance.md index de4168778..0455adff2 100644 --- a/docs/examples/pypsa/generator_maintenance.md +++ b/docs/examples/pypsa/generator_maintenance.md @@ -15,6 +15,7 @@ dimensions: snapshot: description: dispatch periods dtype: datetime + ordered: true generator: description: generating units, each on one bus diff --git a/docs/examples/pypsa/generator_ramping.md b/docs/examples/pypsa/generator_ramping.md index bcb55e430..ae0bf1b3b 100644 --- a/docs/examples/pypsa/generator_ramping.md +++ b/docs/examples/pypsa/generator_ramping.md @@ -15,11 +15,13 @@ dimensions: snapshot: description: dispatch periods dtype: datetime + ordered: true generator: description: generating units, each on one bus period: description: investment periods — PyPSA's `investment_periods` dtype: int + ordered: true relations: snapshot_period: diff --git a/docs/examples/pypsa/line.md b/docs/examples/pypsa/line.md index 6d272bc73..c3416bebd 100644 --- a/docs/examples/pypsa/line.md +++ b/docs/examples/pypsa/line.md @@ -15,6 +15,7 @@ dimensions: snapshot: description: dispatch periods dtype: datetime + ordered: true bus: description: network nodes line: @@ -32,6 +33,7 @@ dimensions: period: description: investment periods — PyPSA's `investment_periods` dtype: int + ordered: true carrier: description: energy carriers, what a growth limit is set per diff --git a/docs/examples/pypsa/link.md b/docs/examples/pypsa/link.md index f6898c494..548cedcfb 100644 --- a/docs/examples/pypsa/link.md +++ b/docs/examples/pypsa/link.md @@ -15,6 +15,7 @@ dimensions: snapshot: description: dispatch periods dtype: datetime + ordered: true bus: description: network nodes link: @@ -29,6 +30,7 @@ dimensions: period: description: investment periods — PyPSA's `investment_periods` dtype: int + ordered: true carrier: description: energy carriers, what a growth limit is set per diff --git a/docs/examples/pypsa/link_commitment.md b/docs/examples/pypsa/link_commitment.md index 894f7b609..685f4deea 100644 --- a/docs/examples/pypsa/link_commitment.md +++ b/docs/examples/pypsa/link_commitment.md @@ -15,11 +15,13 @@ dimensions: snapshot: description: dispatch periods dtype: datetime + ordered: true link: description: controllable connections, each from one bus to the buses it delivers to period: description: investment periods — PyPSA's `investment_periods` dtype: int + ordered: true relations: snapshot_period: diff --git a/docs/examples/pypsa/link_maintenance.md b/docs/examples/pypsa/link_maintenance.md index d8b5ed3de..99ec2472f 100644 --- a/docs/examples/pypsa/link_maintenance.md +++ b/docs/examples/pypsa/link_maintenance.md @@ -15,6 +15,7 @@ dimensions: snapshot: description: dispatch periods dtype: datetime + ordered: true link: description: controllable connections, each from one bus to the buses it delivers to diff --git a/docs/examples/pypsa/link_ramping.md b/docs/examples/pypsa/link_ramping.md index 0b55db30b..d0dd7db1e 100644 --- a/docs/examples/pypsa/link_ramping.md +++ b/docs/examples/pypsa/link_ramping.md @@ -15,11 +15,13 @@ dimensions: snapshot: description: dispatch periods dtype: datetime + ordered: true link: description: controllable connections, each from one bus to the buses it delivers to period: description: investment periods — PyPSA's `investment_periods` dtype: int + ordered: true relations: snapshot_period: diff --git a/docs/examples/pypsa/load.md b/docs/examples/pypsa/load.md index 7b61a4a70..14d443c05 100644 --- a/docs/examples/pypsa/load.md +++ b/docs/examples/pypsa/load.md @@ -15,6 +15,7 @@ dimensions: snapshot: description: dispatch periods dtype: datetime + ordered: true bus: description: network nodes load: diff --git a/docs/examples/pypsa/network.md b/docs/examples/pypsa/network.md index d0d3a9ed3..0fe73c066 100644 --- a/docs/examples/pypsa/network.md +++ b/docs/examples/pypsa/network.md @@ -15,6 +15,7 @@ dimensions: snapshot: description: dispatch periods dtype: datetime + ordered: true bus: description: network nodes diff --git a/docs/examples/pypsa/power_flow.md b/docs/examples/pypsa/power_flow.md index 59bdf5767..395c830f2 100644 --- a/docs/examples/pypsa/power_flow.md +++ b/docs/examples/pypsa/power_flow.md @@ -15,6 +15,7 @@ dimensions: snapshot: description: dispatch periods dtype: datetime + ordered: true cycle: description: independent cycles of the passive network graph — the cycle basis, data prep diff --git a/docs/examples/pypsa/process.md b/docs/examples/pypsa/process.md index a509b5709..eb6ff46d5 100644 --- a/docs/examples/pypsa/process.md +++ b/docs/examples/pypsa/process.md @@ -15,6 +15,7 @@ dimensions: snapshot: description: dispatch periods dtype: datetime + ordered: true bus: description: network nodes process: @@ -29,6 +30,7 @@ dimensions: period: description: investment periods — PyPSA's `investment_periods` dtype: int + ordered: true carrier: description: energy carriers, what a growth limit is set per diff --git a/docs/examples/pypsa/process_commitment.md b/docs/examples/pypsa/process_commitment.md index a64e95ea7..4215c87f4 100644 --- a/docs/examples/pypsa/process_commitment.md +++ b/docs/examples/pypsa/process_commitment.md @@ -15,11 +15,13 @@ dimensions: snapshot: description: dispatch periods dtype: datetime + ordered: true process: description: generalized multi-port converters, each with an internal power that every port draws or delivers at its own rate period: description: investment periods — PyPSA's `investment_periods` dtype: int + ordered: true relations: snapshot_period: diff --git a/docs/examples/pypsa/process_maintenance.md b/docs/examples/pypsa/process_maintenance.md index 0c223e1b2..479385f76 100644 --- a/docs/examples/pypsa/process_maintenance.md +++ b/docs/examples/pypsa/process_maintenance.md @@ -15,6 +15,7 @@ dimensions: snapshot: description: dispatch periods dtype: datetime + ordered: true process: description: generalized multi-port converters, each with an internal power that every port draws or delivers at its own rate diff --git a/docs/examples/pypsa/process_ramping.md b/docs/examples/pypsa/process_ramping.md index ab830860c..220e17e8f 100644 --- a/docs/examples/pypsa/process_ramping.md +++ b/docs/examples/pypsa/process_ramping.md @@ -15,11 +15,13 @@ dimensions: snapshot: description: dispatch periods dtype: datetime + ordered: true process: description: generalized multi-port converters, each with an internal power that every port draws or delivers at its own rate period: description: investment periods — PyPSA's `investment_periods` dtype: int + ordered: true relations: snapshot_period: diff --git a/docs/examples/pypsa/security.md b/docs/examples/pypsa/security.md index 63c13034c..c094ed84c 100644 --- a/docs/examples/pypsa/security.md +++ b/docs/examples/pypsa/security.md @@ -15,6 +15,7 @@ dimensions: snapshot: description: dispatch periods dtype: datetime + ordered: true line: description: passive branches, each between two buses, their flow set by impedance transformer: diff --git a/docs/examples/pypsa/settings.md b/docs/examples/pypsa/settings.md index 2ddbf7db9..64bec3fcf 100644 --- a/docs/examples/pypsa/settings.md +++ b/docs/examples/pypsa/settings.md @@ -15,11 +15,13 @@ dimensions: snapshot: description: dispatch periods dtype: datetime + ordered: true global_constraint: description: PyPSA's `GlobalConstraint` rows, one label per declared limit period: description: investment periods — PyPSA's `investment_periods` dtype: int + ordered: true carrier: description: energy carriers, what a growth limit is set per diff --git a/docs/examples/pypsa/storage_unit.md b/docs/examples/pypsa/storage_unit.md index b4706672f..c0b9816a8 100644 --- a/docs/examples/pypsa/storage_unit.md +++ b/docs/examples/pypsa/storage_unit.md @@ -15,6 +15,7 @@ dimensions: snapshot: description: dispatch periods dtype: datetime + ordered: true bus: description: network nodes storage_unit: @@ -24,6 +25,7 @@ dimensions: period: description: investment periods — PyPSA's `investment_periods` dtype: int + ordered: true carrier: description: energy carriers, what a growth limit is set per diff --git a/docs/examples/pypsa/store.md b/docs/examples/pypsa/store.md index 7d05f8f34..169dd49d5 100644 --- a/docs/examples/pypsa/store.md +++ b/docs/examples/pypsa/store.md @@ -15,6 +15,7 @@ dimensions: snapshot: description: dispatch periods dtype: datetime + ordered: true bus: description: network nodes store: @@ -24,6 +25,7 @@ dimensions: period: description: investment periods — PyPSA's `investment_periods` dtype: int + ordered: true carrier: description: energy carriers, what a growth limit is set per diff --git a/docs/examples/pypsa/transformer.md b/docs/examples/pypsa/transformer.md index b45100aec..c6c4d5cab 100644 --- a/docs/examples/pypsa/transformer.md +++ b/docs/examples/pypsa/transformer.md @@ -15,6 +15,7 @@ dimensions: snapshot: description: dispatch periods dtype: datetime + ordered: true bus: description: network nodes transformer: diff --git a/docs/howto/compose.md b/docs/howto/compose.md index ca3652df0..d501bb9ff 100644 --- a/docs/howto/compose.md +++ b/docs/howto/compose.md @@ -102,7 +102,7 @@ takes its files as a list, and the two compose as `override(merge([…]), […]) ```yaml title="store.yaml" dimensions: - snapshot: { dtype: int } + snapshot: { dtype: int, ordered: true } bus: { dtype: str } store: { dtype: str } relations: diff --git a/docs/howto/see-an-expansion.md b/docs/howto/see-an-expansion.md index e92988ef6..152d00797 100644 --- a/docs/howto/see-an-expansion.md +++ b/docs/howto/see-an-expansion.md @@ -152,7 +152,7 @@ the set out too. ```yaml dimensions: - bp: { dtype: int } + bp: { dtype: int, ordered: true } parameters: x_bp: { dims: [bp] } @@ -209,7 +209,7 @@ the set out too. ```yaml dimensions: - bp: { dtype: int } + bp: { dtype: int, ordered: true } parameters: x_bp: { dims: [bp] } @@ -312,7 +312,7 @@ the set out too. ```yaml dimensions: - bp: { dtype: int } + bp: { dtype: int, ordered: true } parameters: x_bp: { dims: [bp] } diff --git a/docs/reference/language/assumptions.md b/docs/reference/language/assumptions.md index d8d1edb9c..bdb9fa27c 100644 --- a/docs/reference/language/assumptions.md +++ b/docs/reference/language/assumptions.md @@ -57,7 +57,7 @@ includes arithmetic on either side: ```yaml dimensions: - snapshot: { dtype: int } + snapshot: { dtype: int, ordered: true } generator: { dtype: str } parameters: eta: { dims: [generator] } diff --git a/docs/reference/language/dimensions.md b/docs/reference/language/dimensions.md index 922073226..688862d2b 100644 --- a/docs/reference/language/dimensions.md +++ b/docs/reference/language/dimensions.md @@ -13,21 +13,46 @@ onto another is a [relation](relations.md). ```yaml dimensions: - snapshot: { dtype: int } + snapshot: { dtype: int, ordered: true } generator: { dtype: str } ``` Every dimension named anywhere in the file is declared here. -| Field | | | -| ------------- | --------------------------------- | -------------- | -| `dtype` | `float`, `int`, `str`, `datetime` | default `str` | -| `description` | free text | default `null` | +| Field | | | +| ------------- | ----------------------------------------------------- | --------------- | +| `dtype` | `float`, `int`, `str`, `datetime` | default `str` | +| `ordered` | whether the order of the members is part of the model | default `false` | +| `description` | free text | default `null` | The members of a dimension arrive with the data, in the order the table gives -them. [`shift`](operators.md#shift), `sum_back` and `position()` count along -that order, and everything indexed by the dimension is matched to its members -by label. +them. Everything indexed by the dimension is matched to its members by label. + +## Order + +**Only an ordered dimension has an order a construct may read.** A snapshot +comes after the one before it. A generator does not come after another +generator, and a file that steps from one to the next would read the row order +of a data table. Five constructs read the order, and each needs its dimension +declared `ordered: true`: + +| Construct | Reads | +| ---------------------------------------------------------------------------------- | -------------------------------- | +| [`shift(x, along=d, offset=n)`](operators.md#shift), in an expression or a `where` | the member `n` positions back | +| [`sum_back(x, along=d, window=n)`](operators.md#sum_back) | the last `n` members | +| [`position(d)`](expressions.md#position) | where a member sits along `d` | +| [`piecewise:`](piecewise.md) with `over: d` | which breakpoints are neighbours | +| [`sos:`](piecewise.md#sos) with `type: 2` and `along: d` | which members are consecutive | + +The loader refuses any of them along a dimension that is not ordered: + +```text +Constraint 'ramp': shift(along=snapshot) reads the order of 'snapshot', which is not declared ordered, so the order it read would be the row order of the data. Declare the order part of the model with 'snapshot: {ordered: true}' under dimensions:. +``` + +A `dtype` does not make a dimension ordered: an `int` dimension may number +things that have no order. An `sos` block with `type: 1` reads no order, and +`edge='wrap'` makes one `shift` cyclic without changing the dimension. Whether to declare a column of data as a dimension, a relation or a parameter is decided in [declare a column of data](../../howto/declare-a-column.md). diff --git a/docs/reference/language/expressions.md b/docs/reference/language/expressions.md index 1cd2bd90e..9f4d6ef29 100644 --- a/docs/reference/language/expressions.md +++ b/docs/reference/language/expressions.md @@ -280,7 +280,7 @@ is one fewer. A comparison against the previous row gives its `shift` an ```yaml dimensions: - snapshot: { dtype: int } + snapshot: { dtype: int, ordered: true } parameters: load: { dims: [snapshot] } ramp: { dims: [] } @@ -303,7 +303,7 @@ refused everywhere. ```yaml dimensions: - snapshot: { dtype: int } + snapshot: { dtype: int, ordered: true } parameters: soc_initial: { dims: [] } variables: @@ -322,7 +322,7 @@ each period gets one seeded row: ```yaml dimensions: - snapshot: { dtype: int } + snapshot: { dtype: int, ordered: true } period: { dtype: int } relations: period_of: { key: snapshot, values: period } diff --git a/docs/reference/language/operators.md b/docs/reference/language/operators.md index 3842855b0..fbe8c3719 100644 --- a/docs/reference/language/operators.md +++ b/docs/reference/language/operators.md @@ -80,7 +80,7 @@ The dimension **survives**, and a width of `1` is `x` itself. ```yaml dimensions: unit: { dtype: str } - hour: { dtype: int } + hour: { dtype: int, ordered: true } parameters: min_up: { dims: [unit], dtype: int } @@ -107,12 +107,13 @@ window that reaches past the start of the axis is **short**, and no row is lost. ## `shift` -`shift` counts positions in the dimension's **declared order**. `edge=` says +`shift` counts positions in the order of an [ordered](dimensions.md#order) +dimension. `edge=` says what stands where nothing moved in. ```yaml dimensions: - snapshot: { dtype: int } + snapshot: { dtype: int, ordered: true } storage: { dtype: str } parameters: eta: { dims: [storage] } @@ -144,7 +145,7 @@ coordinate is the one before it in its own group, such as a season: ```yaml dimensions: - snapshot: { dtype: int } + snapshot: { dtype: int, ordered: true } season: { dtype: str } relations: season_of: { key: snapshot, values: season } @@ -170,7 +171,7 @@ the data carries as a column: ```yaml dimensions: technology: { dtype: str } - month: { dtype: int } + month: { dtype: int, ordered: true } parameters: lead: { dims: [technology], dtype: int } demand: { dims: [technology, month] } diff --git a/docs/reference/language/piecewise.md b/docs/reference/language/piecewise.md index b48f17716..6a14472b1 100644 --- a/docs/reference/language/piecewise.md +++ b/docs/reference/language/piecewise.md @@ -51,7 +51,8 @@ piecewise: A block states one weight per breakpoint in `[0, 1]`, a row making the weights sum to 1, and a row per link tying its expression to the weighted breakpoints. -The breakpoint order is the declared order of `over`. What a block assumes of +The breakpoint order is the order of `over`, which is an +[ordered](dimensions.md#order) dimension. What a block assumes of its numbers is on [what a curve assumes](assumptions.md#what-a-curve-assumes). ### `activity` @@ -139,7 +140,8 @@ A set is over **one** variable, and a variable holds **one** set. A second block naming the same variable is a load error. A member the variable's `where` masks out is not in the set. The order is the -declared order of the `along` dimension. +order of the `along` dimension, which is +[ordered](dimensions.md#order) for `type: 2`. ### What a set is written out as diff --git a/docs/reference/notation.md b/docs/reference/notation.md index 231f3664a..6cbf45c9e 100644 --- a/docs/reference/notation.md +++ b/docs/reference/notation.md @@ -28,13 +28,13 @@ A dimension, a relation and a parameter declare no equation; what they print is ```yaml dimensions: - snapshot: { dtype: int } - generator: { dtype: str } + snapshot: { dtype: int, ordered: true } + generator: { dtype: str, ordered: true } bus: { dtype: str } zone: { dtype: str } season: { dtype: str } technology: { dtype: str } - bp: { dtype: int } # the breakpoints every curve below runs through + bp: { dtype: int, ordered: true } # the breakpoints every curve below runs through relations: gen_bus: { key: generator, values: bus } diff --git a/docs/reference/reading.md b/docs/reference/reading.md index c10436455..0d101c916 100644 --- a/docs/reference/reading.md +++ b/docs/reference/reading.md @@ -37,7 +37,7 @@ a convexity row and one row per link: ```yaml title="curve.yaml" dimensions: generator: { dtype: str } - bp: { dtype: int } + bp: { dtype: int, ordered: true } parameters: bp_x: { dims: [generator, bp] } bp_y: { dims: [generator, bp] } diff --git a/examples/commitment.yaml b/examples/commitment.yaml index 079c534b5..638e5a9bd 100644 --- a/examples/commitment.yaml +++ b/examples/commitment.yaml @@ -10,7 +10,7 @@ description: >- inequality is written once. dimensions: - snapshot: { dtype: int, description: dispatch periods } + snapshot: { dtype: int, description: dispatch periods, ordered: true } generator: { description: generating units } parameters: diff --git a/examples/operators/shift.yaml b/examples/operators/shift.yaml index 2e31c65e6..33c1bce0a 100644 --- a/examples/operators/shift.yaml +++ b/examples/operators/shift.yaml @@ -7,7 +7,7 @@ description: >- it would have fed is not built. dimensions: - snapshot: { dtype: int } + snapshot: { dtype: int, ordered: true } variables: p: diff --git a/examples/operators/shift_by_parameter.yaml b/examples/operators/shift_by_parameter.yaml index 6be3cb2ea..25482ceb2 100644 --- a/examples/operators/shift_by_parameter.yaml +++ b/examples/operators/shift_by_parameter.yaml @@ -9,7 +9,7 @@ description: >- dimensions: technology: { dtype: str } - month: { dtype: int } + month: { dtype: int, ordered: true } parameters: lead: { dims: [technology], dtype: int } diff --git a/examples/operators/shift_edge.yaml b/examples/operators/shift_edge.yaml index 721f601e8..9fcab13e5 100644 --- a/examples/operators/shift_edge.yaml +++ b/examples/operators/shift_edge.yaml @@ -7,7 +7,7 @@ description: >- number instead of being absent, so the row survives. dimensions: - snapshot: { dtype: int } + snapshot: { dtype: int, ordered: true } variables: p: diff --git a/examples/operators/shift_partitioned.yaml b/examples/operators/shift_partitioned.yaml index 0978b6718..c0ebcfead 100644 --- a/examples/operators/shift_partitioned.yaml +++ b/examples/operators/shift_partitioned.yaml @@ -7,7 +7,7 @@ description: >- snapshot reads that season's last and no level crosses the boundary. dimensions: - snapshot: { dtype: int } + snapshot: { dtype: int, ordered: true } season: { dtype: str } relations: diff --git a/examples/operators/shift_wrap.yaml b/examples/operators/shift_wrap.yaml index 0323f8862..e094ad781 100644 --- a/examples/operators/shift_wrap.yaml +++ b/examples/operators/shift_wrap.yaml @@ -7,7 +7,7 @@ description: >- reads the last and nothing is vacated. dimensions: - snapshot: { dtype: int } + snapshot: { dtype: int, ordered: true } variables: p: diff --git a/examples/operators/sum_back.yaml b/examples/operators/sum_back.yaml index e70afcfb0..d93271f19 100644 --- a/examples/operators/sum_back.yaml +++ b/examples/operators/sum_back.yaml @@ -8,7 +8,7 @@ description: >- dimensions: unit: { dtype: str } - hour: { dtype: int } + hour: { dtype: int, ordered: true } parameters: min_up: { dims: [unit], dtype: int } diff --git a/examples/operators/sum_back_by_parameter.yaml b/examples/operators/sum_back_by_parameter.yaml index 699a0262b..8e4b169bc 100644 --- a/examples/operators/sum_back_by_parameter.yaml +++ b/examples/operators/sum_back_by_parameter.yaml @@ -8,7 +8,7 @@ description: >- dimensions: unit: { dtype: str } - hour: { dtype: int } + hour: { dtype: int, ordered: true } parameters: min_up: { dims: [unit], dtype: int } diff --git a/examples/operators/sum_back_partitioned.yaml b/examples/operators/sum_back_partitioned.yaml index ba03fe961..51043d492 100644 --- a/examples/operators/sum_back_partitioned.yaml +++ b/examples/operators/sum_back_partitioned.yaml @@ -9,7 +9,7 @@ description: >- dimensions: unit: { dtype: str } - hour: { dtype: int } + hour: { dtype: int, ordered: true } day: { dtype: str } relations: diff --git a/examples/operators/sum_back_wrap.yaml b/examples/operators/sum_back_wrap.yaml index 69fea5ec0..b31f4b651 100644 --- a/examples/operators/sum_back_wrap.yaml +++ b/examples/operators/sum_back_wrap.yaml @@ -8,7 +8,7 @@ description: >- dimensions: unit: { dtype: str } - hour: { dtype: int } + hour: { dtype: int, ordered: true } parameters: min_up: { dims: [unit], dtype: int } diff --git a/examples/piecewise.yaml b/examples/piecewise.yaml index c50ccd342..c5ca957ab 100644 --- a/examples/piecewise.yaml +++ b/examples/piecewise.yaml @@ -16,6 +16,7 @@ dimensions: bp: description: breakpoints of the cost curve dtype: int + ordered: true parameters: capacity: diff --git a/examples/piecewise_lp.yaml b/examples/piecewise_lp.yaml index 817c97655..2029aa8dd 100644 --- a/examples/piecewise_lp.yaml +++ b/examples/piecewise_lp.yaml @@ -19,6 +19,7 @@ dimensions: bp: description: breakpoints of the cost curve dtype: int + ordered: true parameters: capacity: diff --git a/examples/piecewise_ragged.yaml b/examples/piecewise_ragged.yaml index 582a28c21..fd6b0ec14 100644 --- a/examples/piecewise_ragged.yaml +++ b/examples/piecewise_ragged.yaml @@ -20,6 +20,7 @@ dimensions: bp: description: breakpoints of the cost curve, as many as the longest curve needs dtype: int + ordered: true parameters: capacity: diff --git a/examples/ports/transport_pwl.yaml b/examples/ports/transport_pwl.yaml index 678aa6fca..71b63b0cb 100644 --- a/examples/ports/transport_pwl.yaml +++ b/examples/ports/transport_pwl.yaml @@ -16,6 +16,7 @@ dimensions: bp: description: breakpoints of the discretised square-root curve dtype: int + ordered: true parameters: capacity: diff --git a/examples/pypsa.yaml b/examples/pypsa.yaml index d52045985..dbd343106 100644 --- a/examples/pypsa.yaml +++ b/examples/pypsa.yaml @@ -23,6 +23,7 @@ dimensions: snapshot: description: dispatch periods dtype: datetime + ordered: true bus: description: network nodes generator: @@ -69,6 +70,7 @@ dimensions: period: description: investment periods — PyPSA's `investment_periods` dtype: int + ordered: true carrier: description: energy carriers, what a growth limit is set per diff --git a/examples/pypsa/carrier.yaml b/examples/pypsa/carrier.yaml index f762b6533..1ef270813 100644 --- a/examples/pypsa/carrier.yaml +++ b/examples/pypsa/carrier.yaml @@ -6,6 +6,7 @@ dimensions: period: description: investment periods — PyPSA's `investment_periods` dtype: int + ordered: true carrier: description: energy carriers, what a growth limit is set per diff --git a/examples/pypsa/generator.yaml b/examples/pypsa/generator.yaml index 3d6308e30..51f501d0f 100644 --- a/examples/pypsa/generator.yaml +++ b/examples/pypsa/generator.yaml @@ -8,6 +8,7 @@ dimensions: snapshot: description: dispatch periods dtype: datetime + ordered: true bus: description: network nodes generator: @@ -17,6 +18,7 @@ dimensions: period: description: investment periods — PyPSA's `investment_periods` dtype: int + ordered: true carrier: description: energy carriers, what a growth limit is set per diff --git a/examples/pypsa/generator_commitment.yaml b/examples/pypsa/generator_commitment.yaml index cbd3c6286..470921b59 100644 --- a/examples/pypsa/generator_commitment.yaml +++ b/examples/pypsa/generator_commitment.yaml @@ -8,11 +8,13 @@ dimensions: snapshot: description: dispatch periods dtype: datetime + ordered: true generator: description: generating units, each on one bus period: description: investment periods — PyPSA's `investment_periods` dtype: int + ordered: true relations: snapshot_period: diff --git a/examples/pypsa/generator_maintenance.yaml b/examples/pypsa/generator_maintenance.yaml index 1f8ca404e..024b6c968 100644 --- a/examples/pypsa/generator_maintenance.yaml +++ b/examples/pypsa/generator_maintenance.yaml @@ -8,6 +8,7 @@ dimensions: snapshot: description: dispatch periods dtype: datetime + ordered: true generator: description: generating units, each on one bus diff --git a/examples/pypsa/generator_ramping.yaml b/examples/pypsa/generator_ramping.yaml index c128b6b68..5b204b322 100644 --- a/examples/pypsa/generator_ramping.yaml +++ b/examples/pypsa/generator_ramping.yaml @@ -8,11 +8,13 @@ dimensions: snapshot: description: dispatch periods dtype: datetime + ordered: true generator: description: generating units, each on one bus period: description: investment periods — PyPSA's `investment_periods` dtype: int + ordered: true relations: snapshot_period: diff --git a/examples/pypsa/line.yaml b/examples/pypsa/line.yaml index ce136665b..28d3ff843 100644 --- a/examples/pypsa/line.yaml +++ b/examples/pypsa/line.yaml @@ -8,6 +8,7 @@ dimensions: snapshot: description: dispatch periods dtype: datetime + ordered: true bus: description: network nodes line: @@ -25,6 +26,7 @@ dimensions: period: description: investment periods — PyPSA's `investment_periods` dtype: int + ordered: true carrier: description: energy carriers, what a growth limit is set per diff --git a/examples/pypsa/link.yaml b/examples/pypsa/link.yaml index 0116a0b63..c1bb6350c 100644 --- a/examples/pypsa/link.yaml +++ b/examples/pypsa/link.yaml @@ -8,6 +8,7 @@ dimensions: snapshot: description: dispatch periods dtype: datetime + ordered: true bus: description: network nodes link: @@ -22,6 +23,7 @@ dimensions: period: description: investment periods — PyPSA's `investment_periods` dtype: int + ordered: true carrier: description: energy carriers, what a growth limit is set per diff --git a/examples/pypsa/link_commitment.yaml b/examples/pypsa/link_commitment.yaml index c90e0ec85..bda92b172 100644 --- a/examples/pypsa/link_commitment.yaml +++ b/examples/pypsa/link_commitment.yaml @@ -8,11 +8,13 @@ dimensions: snapshot: description: dispatch periods dtype: datetime + ordered: true link: description: controllable connections, each from one bus to the buses it delivers to period: description: investment periods — PyPSA's `investment_periods` dtype: int + ordered: true relations: snapshot_period: diff --git a/examples/pypsa/link_maintenance.yaml b/examples/pypsa/link_maintenance.yaml index e81eefbf0..e65f5784f 100644 --- a/examples/pypsa/link_maintenance.yaml +++ b/examples/pypsa/link_maintenance.yaml @@ -8,6 +8,7 @@ dimensions: snapshot: description: dispatch periods dtype: datetime + ordered: true link: description: controllable connections, each from one bus to the buses it delivers to diff --git a/examples/pypsa/link_ramping.yaml b/examples/pypsa/link_ramping.yaml index 4b643a02a..84af71755 100644 --- a/examples/pypsa/link_ramping.yaml +++ b/examples/pypsa/link_ramping.yaml @@ -8,11 +8,13 @@ dimensions: snapshot: description: dispatch periods dtype: datetime + ordered: true link: description: controllable connections, each from one bus to the buses it delivers to period: description: investment periods — PyPSA's `investment_periods` dtype: int + ordered: true relations: snapshot_period: diff --git a/examples/pypsa/load.yaml b/examples/pypsa/load.yaml index e04ef4909..b632a90be 100644 --- a/examples/pypsa/load.yaml +++ b/examples/pypsa/load.yaml @@ -8,6 +8,7 @@ dimensions: snapshot: description: dispatch periods dtype: datetime + ordered: true bus: description: network nodes load: diff --git a/examples/pypsa/network.yaml b/examples/pypsa/network.yaml index f6b0de7ff..2850be116 100644 --- a/examples/pypsa/network.yaml +++ b/examples/pypsa/network.yaml @@ -8,6 +8,7 @@ dimensions: snapshot: description: dispatch periods dtype: datetime + ordered: true bus: description: network nodes diff --git a/examples/pypsa/power_flow.yaml b/examples/pypsa/power_flow.yaml index 1ff17fd94..70ae4a91c 100644 --- a/examples/pypsa/power_flow.yaml +++ b/examples/pypsa/power_flow.yaml @@ -8,6 +8,7 @@ dimensions: snapshot: description: dispatch periods dtype: datetime + ordered: true cycle: description: independent cycles of the passive network graph — the cycle basis, data prep diff --git a/examples/pypsa/process.yaml b/examples/pypsa/process.yaml index 48a4aa26a..0c4999c91 100644 --- a/examples/pypsa/process.yaml +++ b/examples/pypsa/process.yaml @@ -8,6 +8,7 @@ dimensions: snapshot: description: dispatch periods dtype: datetime + ordered: true bus: description: network nodes process: @@ -22,6 +23,7 @@ dimensions: period: description: investment periods — PyPSA's `investment_periods` dtype: int + ordered: true carrier: description: energy carriers, what a growth limit is set per diff --git a/examples/pypsa/process_commitment.yaml b/examples/pypsa/process_commitment.yaml index 35f34e50e..23a1d9907 100644 --- a/examples/pypsa/process_commitment.yaml +++ b/examples/pypsa/process_commitment.yaml @@ -8,11 +8,13 @@ dimensions: snapshot: description: dispatch periods dtype: datetime + ordered: true process: description: generalized multi-port converters, each with an internal power that every port draws or delivers at its own rate period: description: investment periods — PyPSA's `investment_periods` dtype: int + ordered: true relations: snapshot_period: diff --git a/examples/pypsa/process_maintenance.yaml b/examples/pypsa/process_maintenance.yaml index 2f1b9de84..7d751c462 100644 --- a/examples/pypsa/process_maintenance.yaml +++ b/examples/pypsa/process_maintenance.yaml @@ -8,6 +8,7 @@ dimensions: snapshot: description: dispatch periods dtype: datetime + ordered: true process: description: generalized multi-port converters, each with an internal power that every port draws or delivers at its own rate diff --git a/examples/pypsa/process_ramping.yaml b/examples/pypsa/process_ramping.yaml index 7bb61fdfb..65a0d085b 100644 --- a/examples/pypsa/process_ramping.yaml +++ b/examples/pypsa/process_ramping.yaml @@ -8,11 +8,13 @@ dimensions: snapshot: description: dispatch periods dtype: datetime + ordered: true process: description: generalized multi-port converters, each with an internal power that every port draws or delivers at its own rate period: description: investment periods — PyPSA's `investment_periods` dtype: int + ordered: true relations: snapshot_period: diff --git a/examples/pypsa/security.yaml b/examples/pypsa/security.yaml index 79f9de022..c0f9fa04b 100644 --- a/examples/pypsa/security.yaml +++ b/examples/pypsa/security.yaml @@ -8,6 +8,7 @@ dimensions: snapshot: description: dispatch periods dtype: datetime + ordered: true line: description: passive branches, each between two buses, their flow set by impedance transformer: diff --git a/examples/pypsa/settings.yaml b/examples/pypsa/settings.yaml index 39d853dac..3cda64058 100644 --- a/examples/pypsa/settings.yaml +++ b/examples/pypsa/settings.yaml @@ -8,11 +8,13 @@ dimensions: snapshot: description: dispatch periods dtype: datetime + ordered: true global_constraint: description: PyPSA's `GlobalConstraint` rows, one label per declared limit period: description: investment periods — PyPSA's `investment_periods` dtype: int + ordered: true carrier: description: energy carriers, what a growth limit is set per diff --git a/examples/pypsa/storage_unit.yaml b/examples/pypsa/storage_unit.yaml index 9bb491465..4c274e64b 100644 --- a/examples/pypsa/storage_unit.yaml +++ b/examples/pypsa/storage_unit.yaml @@ -8,6 +8,7 @@ dimensions: snapshot: description: dispatch periods dtype: datetime + ordered: true bus: description: network nodes storage_unit: @@ -17,6 +18,7 @@ dimensions: period: description: investment periods — PyPSA's `investment_periods` dtype: int + ordered: true carrier: description: energy carriers, what a growth limit is set per diff --git a/examples/pypsa/store.yaml b/examples/pypsa/store.yaml index 9d0da30aa..0400dd8cb 100644 --- a/examples/pypsa/store.yaml +++ b/examples/pypsa/store.yaml @@ -8,6 +8,7 @@ dimensions: snapshot: description: dispatch periods dtype: datetime + ordered: true bus: description: network nodes store: @@ -17,6 +18,7 @@ dimensions: period: description: investment periods — PyPSA's `investment_periods` dtype: int + ordered: true carrier: description: energy carriers, what a growth limit is set per diff --git a/examples/pypsa/transformer.yaml b/examples/pypsa/transformer.yaml index d04a2d1fb..ebfb413c5 100644 --- a/examples/pypsa/transformer.yaml +++ b/examples/pypsa/transformer.yaml @@ -8,6 +8,7 @@ dimensions: snapshot: description: dispatch periods dtype: datetime + ordered: true bus: description: network nodes transformer: diff --git a/examples/pypsa_linearized_uc.yaml b/examples/pypsa_linearized_uc.yaml index 15c38456b..00772f007 100644 --- a/examples/pypsa_linearized_uc.yaml +++ b/examples/pypsa_linearized_uc.yaml @@ -18,6 +18,7 @@ dimensions: snapshot: description: dispatch periods dtype: datetime + ordered: true bus: description: network nodes generator: diff --git a/examples/sos.yaml b/examples/sos.yaml index 527b22998..89d4a94b5 100644 --- a/examples/sos.yaml +++ b/examples/sos.yaml @@ -16,6 +16,7 @@ dimensions: bp: description: breakpoints of the cost curve dtype: int + ordered: true parameters: capacity: diff --git a/schema/mathspec.schema.json b/schema/mathspec.schema.json index 720bbbad2..0da9a6020 100644 --- a/schema/mathspec.schema.json +++ b/schema/mathspec.schema.json @@ -133,7 +133,7 @@ }, "DimensionBlock": { "additionalProperties": false, - "description": "A declared dimension, and the dtype its coordinates must be.\n\nA dimension is an axis and nothing else: it declares that the axis exists\nand what its coordinates are typed as, never which coordinates there are \u2014\nthose are data, and arrive when the data is attached. The maps its members carry \u2014 a\ngenerator's bus, a snapshot's period \u2014 are top-level ``relations:``\n([`RelationBlock`][]), keyed by their own name.", + "description": "A declared dimension, the dtype its coordinates must be, and whether their order means anything.\n\nA dimension is an axis and nothing else: it declares that the axis exists\nand what its coordinates are typed as, never which coordinates there are \u2014\nthose are data, and arrive when the data is attached. The maps its members carry \u2014 a\ngenerator's bus, a snapshot's period \u2014 are top-level ``relations:``\n([`RelationBlock`][]), keyed by their own name.", "properties": { "description": { "anyOf": [ @@ -157,6 +157,11 @@ ], "title": "Dtype", "type": "string" + }, + "ordered": { + "default": false, + "title": "Ordered", + "type": "boolean" } }, "title": "DimensionBlock", diff --git a/src/mathspec/_expression_resolver.py b/src/mathspec/_expression_resolver.py index ae0630d0f..60adf86d8 100644 --- a/src/mathspec/_expression_resolver.py +++ b/src/mathspec/_expression_resolver.py @@ -31,7 +31,7 @@ shown, ) from mathspec.dimensions import dims_of -from mathspec.errors import DimensionError, SchemaError, did_you_mean +from mathspec.errors import DimensionError, SchemaError, did_you_mean, unordered from mathspec.operators import ( AMOUNTS, BUILTINS, @@ -298,6 +298,9 @@ def _built( named = dims['along'] assert named is not None, 'a translation names the dimension it steps along' (along,) = named + if self.ns.unordered(along): + self.errors.append(unordered(self.context, f'{operator}(along={along})', along)) + return None wrap, fill = edge if edge is not None else (False, None) if operator == 'shift': offset = amounts['offset'] diff --git a/src/mathspec/_where_resolver.py b/src/mathspec/_where_resolver.py index d2d53f1b4..93fe6b6de 100644 --- a/src/mathspec/_where_resolver.py +++ b/src/mathspec/_where_resolver.py @@ -37,7 +37,7 @@ UnresolvedWhereNode, ) from mathspec.dimensions import dims_of, pulled_back_dims -from mathspec.errors import DimensionError, LanguageError, did_you_mean, prefixed +from mathspec.errors import DimensionError, LanguageError, did_you_mean, prefixed, unordered from mathspec.expansion import expand from mathspec.operators import ( PARTITION_NAMES_ITS_GROUP, @@ -212,6 +212,9 @@ def _predicate_call(self, node: UnresolvedPredicateCallNode) -> Predicate | Unre f"'{along.name}'. Write an integer." ) return node + if self.ns.unordered(along.name): + self.errors.append(unordered(context, f'shift(, along={along.name})', along.name)) + return node mask = Mask(operand) if along.name not in mask.dims: self.errors.append( @@ -417,6 +420,9 @@ def _position( f'{did_you_mean(dimension, ns.dimensions, label="Dimensions")}' ) return node + if ns.unordered(dimension): + self.errors.append(unordered(context, f'position({dimension})', dimension)) + return node if by is None: return DimensionPosition(dimension, node.op, position) if (problem := self._expressions.not_a_relation(by, 'position', 'by')) is not None: diff --git a/src/mathspec/errors.py b/src/mathspec/errors.py index 7a81d1ec8..6da5172a0 100644 --- a/src/mathspec/errors.py +++ b/src/mathspec/errors.py @@ -68,6 +68,15 @@ def did_you_mean(name: str, known: Iterable[str], *, label: str = 'Declared', li return f'{label}: {", ".join(candidates) or "nothing"}.' if listing else '' +def unordered(context: str, construct: str, dimension: str) -> str: + """The refusal for *construct* reading the order of *dimension*, which is not declared ordered.""" + return ( + f"{context}: {construct} reads the order of '{dimension}', which is not declared ordered, so the " + f'order it read would be the row order of the data. Declare the order part of the model with ' + f"'{dimension}: {{ordered: true}}' under dimensions:." + ) + + def schema_error(exc: ValidationError) -> LanguageError: """A pydantic ``ValidationError`` as one of ours. diff --git a/src/mathspec/resolution.py b/src/mathspec/resolution.py index 6308650e5..181af3fa6 100644 --- a/src/mathspec/resolution.py +++ b/src/mathspec/resolution.py @@ -204,6 +204,16 @@ def _references(self, name: str) -> tuple[str, ...]: names = (n.name for n in nodes(*arithmetic) if isinstance(n, NameNode) and n.name in self.bodies) return tuple(dict.fromkeys(names)) + def unordered(self, name: str) -> bool: + """Whether *name* is a declared dimension whose order the file does not declare part of the model. + + A name no dimension declares is not one: it is a macro's formal, whose + dimension is checked where the macro is called, or a name refused + elsewhere. + """ + declared = self.schema.dimensions.get(name) + return declared is not None and not declared.ordered + def kind(self, name: str) -> DeclarationKind | None: """What *name* was declared as, or ``None`` where the file declares it nowhere.""" if name in self.variables: diff --git a/src/mathspec/spec.py b/src/mathspec/spec.py index ae1767f26..3abd0d97c 100644 --- a/src/mathspec/spec.py +++ b/src/mathspec/spec.py @@ -191,7 +191,7 @@ def value_roles(self) -> tuple[str, ...]: class DimensionBlock(_StrictBlock): - """A declared dimension, and the dtype its coordinates must be. + """A declared dimension, the dtype its coordinates must be, and whether their order means anything. A dimension is an axis and nothing else: it declares that the axis exists and what its coordinates are typed as, never which coordinates there are — @@ -203,8 +203,20 @@ class DimensionBlock(_StrictBlock): _label: ClassVar[str] = 'a dimension declaration' dtype: DimensionDtype = 'str' + #: Whether the order the data gives the coordinates in is part of the + #: model. Only an ordered dimension is one a construct may step or count + #: along; every other one is a set whose row order means nothing. + ordered: bool = False description: str | None = None + @model_serializer(mode='wrap') + def _as_written(self, handler: SerializerFunctionWrapHandler) -> dict[str, object]: + """``ordered`` is written where it is true, as ``empty`` is: false is what leaving it out says.""" + written = cast('dict[str, object]', handler(self)) + if not self.ordered: + del written['ordered'] + return written + class ParameterBlock(_StrictBlock): """A declared parameter with dims and dtype.""" diff --git a/src/mathspec/validation.py b/src/mathspec/validation.py index 3b7424ae6..32f3bc56a 100644 --- a/src/mathspec/validation.py +++ b/src/mathspec/validation.py @@ -23,7 +23,7 @@ from typing import TYPE_CHECKING from mathspec._yaml import read_spec -from mathspec.errors import SchemaError +from mathspec.errors import SchemaError, unordered from mathspec.operators import BUILTIN_NAMES from mathspec.piecewise import Emitted as EmittedCurve from mathspec.sos import Emitted as EmittedSet @@ -257,6 +257,8 @@ def _sos_shapes(schema: Spec) -> Iterator[str]: f"'{block.variable}' (dims {schema.variables[block.variable].dims}). The set runs " f"along one of the variable's own dims — one set per coordinate of the rest." ) + elif block.type == 2 and not schema.dimensions[block.along].ordered: + yield unordered(context, f'type: 2 along {block.along}', block.along) elif block.variable in claimed: yield ( f"{context}: variable '{block.variable}' already carries the set declared by " @@ -302,6 +304,8 @@ def _piecewise_references(schema: Spec) -> Iterator[str]: if pw.over not in schema.dimensions: yield undeclared_dimension('piecewise', name, pw.over) continue + if not schema.dimensions[pw.over].ordered: + yield unordered(context, f'over: {pw.over}', pw.over) for i, link in enumerate(pw.links): if link.values not in schema.parameters: yield f"{context}: link {i} values references undeclared parameter '{link.values}'" diff --git a/tests/expand/curve-activity/after.yaml b/tests/expand/curve-activity/after.yaml index 4ec420855..21767569c 100644 --- a/tests/expand/curve-activity/after.yaml +++ b/tests/expand/curve-activity/after.yaml @@ -1,5 +1,5 @@ dimensions: - bp: { dtype: int } + bp: { dtype: int, ordered: true } parameters: x_bp: { dims: [bp] } diff --git a/tests/expand/curve-activity/before.yaml b/tests/expand/curve-activity/before.yaml index 41948c3dc..75f852ea2 100644 --- a/tests/expand/curve-activity/before.yaml +++ b/tests/expand/curve-activity/before.yaml @@ -1,5 +1,5 @@ dimensions: - bp: { dtype: int } + bp: { dtype: int, ordered: true } parameters: x_bp: { dims: [bp] } diff --git a/tests/expand/curve-adjacency-points/after.yaml b/tests/expand/curve-adjacency-points/after.yaml index 0dd6a7d6f..1ce7bc261 100644 --- a/tests/expand/curve-adjacency-points/after.yaml +++ b/tests/expand/curve-adjacency-points/after.yaml @@ -1,5 +1,5 @@ dimensions: - bp: { dtype: int } + bp: { dtype: int, ordered: true } parameters: x_bp: { dims: [bp] } diff --git a/tests/expand/curve-adjacency-points/before.yaml b/tests/expand/curve-adjacency-points/before.yaml index 45b82dfe8..6573f78b4 100644 --- a/tests/expand/curve-adjacency-points/before.yaml +++ b/tests/expand/curve-adjacency-points/before.yaml @@ -1,5 +1,5 @@ dimensions: - bp: { dtype: int } + bp: { dtype: int, ordered: true } parameters: x_bp: { dims: [bp] } diff --git a/tests/expand/curve-adjacency/after.yaml b/tests/expand/curve-adjacency/after.yaml index 317493b6e..3b607d0f9 100644 --- a/tests/expand/curve-adjacency/after.yaml +++ b/tests/expand/curve-adjacency/after.yaml @@ -1,5 +1,5 @@ dimensions: - bp: { dtype: int } + bp: { dtype: int, ordered: true } parameters: x_bp: { dims: [bp] } diff --git a/tests/expand/curve-adjacency/before.yaml b/tests/expand/curve-adjacency/before.yaml index 133038e25..10d771f74 100644 --- a/tests/expand/curve-adjacency/before.yaml +++ b/tests/expand/curve-adjacency/before.yaml @@ -1,5 +1,5 @@ dimensions: - bp: { dtype: int } + bp: { dtype: int, ordered: true } parameters: x_bp: { dims: [bp] } diff --git a/tests/expand/curve-convex/after.yaml b/tests/expand/curve-convex/after.yaml index 4d69388a3..05e0f3b5e 100644 --- a/tests/expand/curve-convex/after.yaml +++ b/tests/expand/curve-convex/after.yaml @@ -1,5 +1,5 @@ dimensions: - bp: { dtype: int } + bp: { dtype: int, ordered: true } parameters: x_bp: { dims: [bp] } diff --git a/tests/expand/curve-convex/before.yaml b/tests/expand/curve-convex/before.yaml index 269b47614..c41665e9d 100644 --- a/tests/expand/curve-convex/before.yaml +++ b/tests/expand/curve-convex/before.yaml @@ -1,5 +1,5 @@ dimensions: - bp: { dtype: int } + bp: { dtype: int, ordered: true } parameters: x_bp: { dims: [bp] } diff --git a/tests/expand/curve-lp-points/after.yaml b/tests/expand/curve-lp-points/after.yaml index 9a24e7b0d..6918dbe25 100644 --- a/tests/expand/curve-lp-points/after.yaml +++ b/tests/expand/curve-lp-points/after.yaml @@ -1,5 +1,5 @@ dimensions: - bp: { dtype: int } + bp: { dtype: int, ordered: true } parameters: x_bp: { dims: [bp] } diff --git a/tests/expand/curve-lp-points/before.yaml b/tests/expand/curve-lp-points/before.yaml index 17748873c..8c9210046 100644 --- a/tests/expand/curve-lp-points/before.yaml +++ b/tests/expand/curve-lp-points/before.yaml @@ -1,5 +1,5 @@ dimensions: - bp: { dtype: int } + bp: { dtype: int, ordered: true } parameters: x_bp: { dims: [bp] } diff --git a/tests/expand/curve-lp/after.yaml b/tests/expand/curve-lp/after.yaml index 0c7f627bc..961a6861c 100644 --- a/tests/expand/curve-lp/after.yaml +++ b/tests/expand/curve-lp/after.yaml @@ -1,5 +1,5 @@ dimensions: - bp: { dtype: int } + bp: { dtype: int, ordered: true } parameters: x_bp: { dims: [bp] } diff --git a/tests/expand/curve-lp/before.yaml b/tests/expand/curve-lp/before.yaml index 7f2815334..0d2330ef4 100644 --- a/tests/expand/curve-lp/before.yaml +++ b/tests/expand/curve-lp/before.yaml @@ -1,5 +1,5 @@ dimensions: - bp: { dtype: int } + bp: { dtype: int, ordered: true } parameters: x_bp: { dims: [bp] } diff --git a/tests/expand/curve-sos2-piecewise/after.yaml b/tests/expand/curve-sos2-piecewise/after.yaml index 2efdb329b..ff4466034 100644 --- a/tests/expand/curve-sos2-piecewise/after.yaml +++ b/tests/expand/curve-sos2-piecewise/after.yaml @@ -1,5 +1,5 @@ dimensions: - bp: { dtype: int } + bp: { dtype: int, ordered: true } parameters: x_bp: { dims: [bp] } diff --git a/tests/expand/curve-sos2-piecewise/before.yaml b/tests/expand/curve-sos2-piecewise/before.yaml index c3c82ec71..7cb049498 100644 --- a/tests/expand/curve-sos2-piecewise/before.yaml +++ b/tests/expand/curve-sos2-piecewise/before.yaml @@ -1,5 +1,5 @@ dimensions: - bp: { dtype: int } + bp: { dtype: int, ordered: true } parameters: x_bp: { dims: [bp] } diff --git a/tests/expand/curve-sos2/after.yaml b/tests/expand/curve-sos2/after.yaml index 317493b6e..3b607d0f9 100644 --- a/tests/expand/curve-sos2/after.yaml +++ b/tests/expand/curve-sos2/after.yaml @@ -1,5 +1,5 @@ dimensions: - bp: { dtype: int } + bp: { dtype: int, ordered: true } parameters: x_bp: { dims: [bp] } diff --git a/tests/expand/curve-sos2/before.yaml b/tests/expand/curve-sos2/before.yaml index c3c82ec71..7cb049498 100644 --- a/tests/expand/curve-sos2/before.yaml +++ b/tests/expand/curve-sos2/before.yaml @@ -1,5 +1,5 @@ dimensions: - bp: { dtype: int } + bp: { dtype: int, ordered: true } parameters: x_bp: { dims: [bp] } diff --git a/tests/expand/set-type2/after.yaml b/tests/expand/set-type2/after.yaml index 0fa7a4c63..583f060ed 100644 --- a/tests/expand/set-type2/after.yaml +++ b/tests/expand/set-type2/after.yaml @@ -1,5 +1,5 @@ dimensions: - g: { dtype: str } + g: { dtype: str, ordered: true } variables: p: diff --git a/tests/expand/set-type2/before.yaml b/tests/expand/set-type2/before.yaml index 1a6d936be..ae4a2eac7 100644 --- a/tests/expand/set-type2/before.yaml +++ b/tests/expand/set-type2/before.yaml @@ -1,5 +1,5 @@ dimensions: - g: { dtype: str } + g: { dtype: str, ordered: true } variables: p: diff --git a/tests/fixtures.py b/tests/fixtures.py index e71cd3ce6..c9fb741be 100644 --- a/tests/fixtures.py +++ b/tests/fixtures.py @@ -32,7 +32,7 @@ #: names, so a test that prints it asserts on the math rather than on the #: example's own vocabulary. DISPATCH_MODEL: dict[str, Any] = { - 'dimensions': {'snapshot': {'dtype': 'int'}, 'generator': {'dtype': 'str'}}, + 'dimensions': {'snapshot': {'dtype': 'int', 'ordered': True}, 'generator': {'dtype': 'str'}}, 'parameters': { 'p_max': {'dims': ['generator']}, 'cost': {'dims': ['generator']}, @@ -48,7 +48,7 @@ #: a rule can name, and no objective, so a test adds what it judges. `p` and `r` #: share no dimension, which is what a rule about *different* dims needs. SMALL_MODEL: dict[str, Any] = { - 'dimensions': {'g': {'dtype': 'str'}, 'h': {'dtype': 'str'}}, + 'dimensions': {'g': {'dtype': 'str', 'ordered': True}, 'h': {'dtype': 'str', 'ordered': True}}, 'relations': {'lk': {'key': 'g', 'values': 'h'}}, 'parameters': { 'c': {'dims': ['g']}, diff --git a/tests/fixtures/every_program_node.yaml b/tests/fixtures/every_program_node.yaml index c318c482f..b054773c2 100644 --- a/tests/fixtures/every_program_node.yaml +++ b/tests/fixtures/every_program_node.yaml @@ -4,7 +4,7 @@ description: every node a program can carry, in one file that lowers dimensions: - t: { dtype: int } + t: { dtype: int, ordered: true } g: { dtype: str } zone: { dtype: str } relations: diff --git a/tests/test_advice.py b/tests/test_advice.py index 679d491f4..6a6710b4b 100644 --- a/tests/test_advice.py +++ b/tests/test_advice.py @@ -38,7 +38,7 @@ CURVED = varied( UNREACHED, objective={'sense': 'minimize', 'expression': 'sum(p)'}, - dimensions={'g': {'dtype': 'str'}, 'h': {'dtype': 'str'}, 'bp': {'dtype': 'int'}}, + dimensions={'g': {'dtype': 'str'}, 'h': {'dtype': 'str'}, 'bp': {'dtype': 'int', 'ordered': True}}, parameters={'c': {'dims': ['g']}, 'bp_x': {'dims': ['bp']}, 'bp_y': {'dims': ['bp']}}, variables={'p': {'dims': ['g']}, 'cost': {'dims': ['g']}}, piecewise={'curve': {'over': 'bp', 'links': [['p', 'bp_x'], ['cost', 'bp_y']]}}, diff --git a/tests/test_boundedness.py b/tests/test_boundedness.py index 1d456cc9d..8560bc2bc 100644 --- a/tests/test_boundedness.py +++ b/tests/test_boundedness.py @@ -103,7 +103,7 @@ def test_a_named_constant_coefficient_carries_its_sign(objective, side): ), pytest.param( { - 'dimensions.bp': {'dtype': 'int'}, + 'dimensions.bp': {'dtype': 'int', 'ordered': True}, 'parameters.bp_x': {'dims': ['bp']}, 'parameters.bp_y': {'dims': ['bp']}, 'piecewise': {'curve': {'over': 'bp', 'links': [['v', 'bp_x'], ['w', 'bp_y']]}}, diff --git a/tests/test_canonical.py b/tests/test_canonical.py index 2b645f47e..54edf0e34 100644 --- a/tests/test_canonical.py +++ b/tests/test_canonical.py @@ -491,14 +491,20 @@ def test_the_form_declares_the_same_spec(path): def test_a_named_expression_written_on_one_line_is_normalised_too(): """`name: a + b` serialises back as a bare string, which the form passed through as written.""" - frame = {'dimensions': {'t': {'dtype': 'int'}}, 'variables': {'a': {'dims': ['t']}, 'b': {'dims': ['t']}}} + frame = { + 'dimensions': {'t': {'dtype': 'int', 'ordered': True}}, + 'variables': {'a': {'dims': ['t']}, 'b': {'dims': ['t']}}, + } one, other = ({**frame, 'expressions': {'total': text}} for text in ('a + b', 'b + a')) assert ms.to_spec(one).to_yaml(canonical=True) == ms.to_spec(other).to_yaml(canonical=True) def test_the_names_a_file_reads_are_sorted_like_the_names_it_declares(): """`given:` nests its kinds one level below a section, so sorting the sections alone left them in file order.""" - frame = {'dimensions': {'t': {'dtype': 'int'}}, 'constraints': {'c': {'dims': ['t'], 'expression': 'a + b >= 0'}}} + frame = { + 'dimensions': {'t': {'dtype': 'int', 'ordered': True}}, + 'constraints': {'c': {'dims': ['t'], 'expression': 'a + b >= 0'}}, + } one, other = ( {**frame, 'given': {'variables': dict(entries)}} for entries in ( diff --git a/tests/test_composition.py b/tests/test_composition.py index ec2e96d14..caabaeb9e 100644 --- a/tests/test_composition.py +++ b/tests/test_composition.py @@ -448,7 +448,7 @@ def test_a_later_patch_is_laid_on_what_the_earlier_ones_make(patches, field, val def test_a_patch_adds_a_dimension_and_may_restate_one_it_shares(): laid = override( DISPATCH_MODEL, - [{'dimensions': {'snapshot': {'dtype': 'int'}, 'investment_period': {'dtype': 'int'}}}], + [{'dimensions': {'snapshot': {'dtype': 'int', 'ordered': True}, 'investment_period': {'dtype': 'int'}}}], ) assert sorted(laid.dimensions) == ['generator', 'investment_period', 'snapshot'], ( 'the dimension the patch adds joins the two the base declares, and the restated one is not doubled' diff --git a/tests/test_dimensions.py b/tests/test_dimensions.py index da8ef4d37..be12e235c 100644 --- a/tests/test_dimensions.py +++ b/tests/test_dimensions.py @@ -28,8 +28,8 @@ #: over a dim `p` does not carry, so it is readable only through a `by=`. BASE = { 'dimensions': { - 'snapshot': {'dtype': 'int'}, - 'generator': {'dtype': 'str'}, + 'snapshot': {'dtype': 'int', 'ordered': True}, + 'generator': {'dtype': 'str', 'ordered': True}, 'bus': {'dtype': 'str'}, 'zone': {'dtype': 'str'}, }, @@ -453,7 +453,7 @@ class TestTheEdgeRulesAreDecidedAtLoad: """ BASE: ClassVar[dict[str, Any]] = { - 'dimensions': {'t': {'dtype': 'int'}, 'g': {'dtype': 'str'}}, + 'dimensions': {'t': {'dtype': 'int', 'ordered': True}, 'g': {'dtype': 'str', 'ordered': True}}, 'parameters': {'cap': {'dims': ['g']}, 'lead': {'dims': ['g'], 'dtype': 'int'}}, 'variables': {'p': {'dims': ['t', 'g'], 'bounds': {'lower': 0, 'upper': 1}}}, 'constraints': {'k': {'dims': ['t', 'g'], 'expression': 'p <= 1'}}, diff --git a/tests/test_exclusivity.py b/tests/test_exclusivity.py index b7c8a38dc..d17a7fc33 100644 --- a/tests/test_exclusivity.py +++ b/tests/test_exclusivity.py @@ -31,8 +31,8 @@ #: Every axis takes its coordinates from data, so nothing here sizes one. STORAGE: dict[str, Any] = { 'dimensions': { - 'snapshot': {'dtype': 'int'}, - 'storage': {}, + 'snapshot': {'dtype': 'int', 'ordered': True}, + 'storage': {'ordered': True}, 'period': {'dtype': 'int'}, }, 'relations': {'period_of': {'key': 'snapshot', 'values': 'period'}}, diff --git a/tests/test_lowering.py b/tests/test_lowering.py index e92a72619..2f925f82c 100644 --- a/tests/test_lowering.py +++ b/tests/test_lowering.py @@ -73,7 +73,7 @@ #: the smallest model that loads, for a claim about the plan's record rather #: than about the math in it. A test adds what it judges with :func:`varied`. TINY = { - 'dimensions': {'g': {}}, + 'dimensions': {'g': {'ordered': True}}, 'parameters': {'cost': {'dims': ['g']}}, 'variables': {'p': {'dims': ['g'], 'bounds': {'lower': 0, 'upper': 1}}}, 'constraints': {'c': {'dims': [], 'expression': 'sum(p, over=g) >= 1'}}, @@ -163,9 +163,9 @@ def test_a_file_with_no_objective_lowers_to_no_sense(): assert program.objective is None, 'no objective declared is no objective, not a minimisation of nothing' -def test_a_literal_amount_resolves_to_one_signed_number(dispatch_schema): +def test_a_literal_amount_resolves_to_one_signed_number(): """`offset=-1` parses as a unary minus over `1`; after resolution it is `-1`, for every reader alike.""" - ns = Namespace(dispatch_schema) + ns = Namespace(schema_of(DISPATCH_YAML, **{'dimensions.snapshot': {'dtype': 'int', 'ordered': True}})) node = expression_of('shift(dispatch, along=snapshot, offset=-1, edge=+0)', ns, 't') assert isinstance(node, Translate) assert (node.offset, node.fill) == (-1, 0.0) @@ -650,7 +650,7 @@ def test_a_partition_keeps_its_group_when_the_relation_gains_a_value_column(): for values in ('day', ['day', 'week']): program = to_spec( { - 'dimensions': {'hour': {'dtype': 'int'}, 'day': {}, 'week': {}}, + 'dimensions': {'hour': {'dtype': 'int', 'ordered': True}, 'day': {}, 'week': {}}, 'relations': {'cal': {'key': 'hour', 'values': values}}, 'variables': {'p': {'dims': ['hour']}}, 'constraints': { @@ -678,7 +678,7 @@ def test_a_relation_lowers_with_the_direction_each_call_names(): """Every node reading a relation carries its columns, its key and the direction, so a consumer joins on the right columns.""" program = to_spec( { - 'dimensions': {'snapshot': {'dtype': 'int'}, 'generator': {}, 'zone': {}}, + 'dimensions': {'snapshot': {'dtype': 'int', 'ordered': True}, 'generator': {'ordered': True}, 'zone': {}}, 'relations': {'zone_of': {'key': ['generator', 'snapshot'], 'values': 'zone'}}, 'parameters': {'price': {'dims': ['snapshot', 'zone']}}, 'variables': { @@ -937,7 +937,7 @@ def test_a_dimension_carries_the_dtype_its_labels_are_checked_against(): CASED = { - 'dimensions': {'t': {'dtype': 'int'}, 'g': {'dtype': 'str'}}, + 'dimensions': {'t': {'dtype': 'int', 'ordered': True}, 'g': {'dtype': 'str', 'ordered': True}}, 'parameters': {'committable': {'dims': ['g'], 'dtype': 'bool'}, 'initial': {'dims': ['g']}}, 'variables': {'status': {'dims': ['t', 'g'], 'domain': 'binary'}}, 'expressions': { @@ -1115,7 +1115,7 @@ def test_a_lowered_spec_still_pickles_and_lowers_to_the_same_program(): spec = Spec.model_validate( { - 'dimensions': {'t': {'dtype': 'int'}, 'g': {'dtype': 'str'}}, + 'dimensions': {'t': {'dtype': 'int', 'ordered': True}, 'g': {'dtype': 'str', 'ordered': True}}, 'parameters': {'load': {'dims': ['t']}, 'cost': {'dims': ['g']}}, 'variables': {'p': {'dims': ['t', 'g'], 'bounds': {'lower': 0}}}, 'constraints': {'balance': {'dims': ['t'], 'expression': 'sum(p, over=g) >= load'}}, @@ -1143,7 +1143,7 @@ def test_a_lowered_program_pickles_and_is_the_same_program(): program = to_spec( { - 'dimensions': {'t': {'dtype': 'int'}, 'g': {'dtype': 'str'}}, + 'dimensions': {'t': {'dtype': 'int', 'ordered': True}, 'g': {'dtype': 'str', 'ordered': True}}, 'parameters': {'load': {'dims': ['t']}, 'cost': {'dims': ['g']}}, 'variables': {'p': {'dims': ['t', 'g'], 'bounds': {'lower': 0}}}, 'constraints': {'balance': {'dims': ['t'], 'expression': 'sum(p, over=g) >= load'}}, @@ -1167,7 +1167,7 @@ def test_two_groups_of_a_program_merge_with_or_as_they_did_behind_the_proxy(): where the seal answered `|` with a `TypeError`.""" program = to_spec( { - 'dimensions': {'t': {'dtype': 'int'}}, + 'dimensions': {'t': {'dtype': 'int', 'ordered': True}}, 'parameters': {'load': {'dims': ['t']}}, 'variables': {'p': {'dims': ['t'], 'bounds': {'lower': 0}}}, 'constraints': {'meet': {'dims': ['t'], 'expression': 'p >= load'}}, diff --git a/tests/test_piecewise.py b/tests/test_piecewise.py index eed8ecde0..b58d9166b 100644 --- a/tests/test_piecewise.py +++ b/tests/test_piecewise.py @@ -25,8 +25,8 @@ #: binaries and links is not something a smaller one can stand in for. NONCONVEX_YAML = """ dimensions: - snapshot: {dtype: int} - bp: {dtype: int} + snapshot: {dtype: int, ordered: true} + bp: {dtype: int, ordered: true} parameters: load: {dims: [snapshot]} diff --git a/tests/test_separability.py b/tests/test_separability.py index ca399b782..e917fff24 100644 --- a/tests/test_separability.py +++ b/tests/test_separability.py @@ -24,7 +24,12 @@ FIXTURE = Path(__file__).resolve().parent / 'fixtures' / 'every_program_node.yaml' BASE: dict[str, Any] = { - 'dimensions': {'h': {'dtype': 'int'}, 'u': {'dtype': 'str'}, 'zone': {'dtype': 'str'}, 'day': {'dtype': 'int'}}, + 'dimensions': { + 'h': {'dtype': 'int', 'ordered': True}, + 'u': {'dtype': 'str', 'ordered': True}, + 'zone': {'dtype': 'str'}, + 'day': {'dtype': 'int'}, + }, 'relations': {'zone_of': {'key': 'u', 'values': 'zone'}, 'day_of': {'key': 'h', 'values': 'day'}}, 'parameters': { 'cost': {'dims': ['u']}, diff --git a/tests/test_sos.py b/tests/test_sos.py index 9da45d48a..7f15d593f 100644 --- a/tests/test_sos.py +++ b/tests/test_sos.py @@ -29,7 +29,7 @@ #: The curve of `examples/sos.yaml`, as a dict a test can vary. CURVE = { - 'dimensions': {'snapshot': {'dtype': 'int'}, 'bp': {'dtype': 'int'}}, + 'dimensions': {'snapshot': {'dtype': 'int'}, 'bp': {'dtype': 'int', 'ordered': True}}, 'parameters': {'load': {'dims': ['snapshot']}, 'bp_x': {'dims': ['bp']}, 'bp_y': {'dims': ['bp']}}, 'variables': { 'p': {'dims': ['snapshot'], 'bounds': {'lower': 0, 'upper': 100}}, diff --git a/tests/test_validation.py b/tests/test_validation.py index 099f9e522..e60aa85c9 100644 --- a/tests/test_validation.py +++ b/tests/test_validation.py @@ -210,7 +210,7 @@ def _kwarg_model(expression: str, dims: list[str] | None = None) -> dict[str, An """ return { 'dimensions': { - 'snapshot': {'dtype': 'int'}, + 'snapshot': {'dtype': 'int', 'ordered': True}, 'bus': {'dtype': 'str'}, 'generator': {'dtype': 'str'}, }, @@ -554,7 +554,7 @@ def test_the_version_gates_no_behaviour(self): #: `position(dim)` needs a relation over *that* dimension, so one over it and one into it. POSITION_SCHEMA = to_spec( { - 'dimensions': {'snapshot': {'dtype': 'int'}, 'period': {'dtype': 'int'}}, + 'dimensions': {'snapshot': {'dtype': 'int', 'ordered': True}, 'period': {'dtype': 'int'}}, 'relations': { 'period_of': {'key': 'snapshot', 'values': 'period'}, 'starts_at': {'key': 'period', 'values': 'snapshot'}, @@ -1805,7 +1805,7 @@ def test_a_default_is_written_out_and_an_absence_is_not(self): #: A model with room for a cased expression: two dimensions, so an arm can be #: narrower than the frame, and a variable, so an arm can reach one. CASED_BASE = { - 'dimensions': {'snapshot': {'dtype': 'int'}, 'generator': {}}, + 'dimensions': {'snapshot': {'dtype': 'int', 'ordered': True}, 'generator': {}}, 'parameters': {'p_max': {'dims': ['generator']}, 'load': {'dims': ['snapshot']}}, 'variables': {'p': {'dims': ['snapshot', 'generator']}}, } @@ -2204,7 +2204,7 @@ def record(*args, **kwargs): 'otherwise': 'p_max - p', }, 'constraints.spare': {'dims': ['snapshot', 'generator'], 'expression': 'p <= headroom'}, - 'dimensions.bp': {'dtype': 'int'}, + 'dimensions.bp': {'dtype': 'int', 'ordered': True}, 'parameters.bp_x': {'dims': ['generator', 'bp']}, 'parameters.bp_y': {'dims': ['generator', 'bp']}, 'variables.op_cost': {'dims': ['snapshot', 'generator'], 'bounds': {'lower': 0}}, @@ -2281,3 +2281,76 @@ def test_an_infinite_bound_is_refused_with_the_null_that_opens_a_side(side, valu message = _refusal(DISPATCH_MODEL, **{f'variables.p.bounds.{side}': value}) assert f'bounds.{side} is {value}, and a bound is finite' in message assert f'{side}: null' in message, 'the refusal names the spelling of an open side' + + +#: One dimension every construct below reads the order of, and a breakpoint +#: dimension for the two formulations; neither declares its order. +UNORDERED: dict[str, Any] = { + 'dimensions': {'t': {'dtype': 'int'}, 'bp': {'dtype': 'int'}}, + 'parameters': { + 'on': {'dims': ['t'], 'dtype': 'bool'}, + 'bp_x': {'dims': ['bp']}, + 'bp_y': {'dims': ['bp']}, + }, + 'variables': { + 'x': {'dims': ['t'], 'bounds': {'lower': 0, 'upper': 1}}, + 'y': {'dims': ['t'], 'bounds': {'lower': 0, 'upper': 1}}, + 'z': {'dims': ['bp'], 'bounds': {'lower': 0, 'upper': 1}}, + }, +} + +_READS_ORDER = [ + pytest.param( + {'constraints.c': {'dims': ['t'], 'expression': 'x <= shift(x, along=t, offset=1, edge=0)'}}, + 't', + id='shift', + ), + pytest.param( + {'constraints.c': {'dims': ['t'], 'expression': 'x <= sum_back(y, along=t, window=2)'}}, + 't', + id='sum_back', + ), + pytest.param( + {'constraints.c': {'dims': ['t'], 'where': 'shift(on, along=t, offset=1)', 'expression': 'x <= 0'}}, + 't', + id='a-where-shift', + ), + pytest.param( + {'constraints.c': {'dims': ['t'], 'where': 'position(t) == 0', 'expression': 'x <= 0'}}, + 't', + id='position', + ), + pytest.param( + {'piecewise.curve': {'over': 'bp', 'links': [['x', 'bp_x'], ['y', 'bp_y']]}}, + 'bp', + id='a-piecewise-curve', + ), + pytest.param({'sos.s': {'variable': 'z', 'along': 'bp', 'type': 2}}, 'bp', id='a-type-2-set'), +] + + +@pytest.mark.parametrize(('patch', 'dimension'), _READS_ORDER) +def test_a_construct_that_reads_order_along_an_unordered_dimension_is_refused(patch, dimension): + """The order an unordered dimension has is the data's row order, which is not part of the model.""" + message = _refusal(UNORDERED, **patch) + assert f"reads the order of '{dimension}', which is not declared ordered" in message + assert f"'{dimension}: {{ordered: true}}' under dimensions:" in message, 'the refusal names the rewrite' + + +@pytest.mark.parametrize(('patch', 'dimension'), _READS_ORDER) +def test_the_same_construct_along_an_ordered_dimension_loads(patch, dimension): + ordered = varied(UNORDERED, **{f'dimensions.{dimension}.ordered': True}) + to_spec(varied(ordered, **patch)) + + +def test_a_type_1_set_reads_no_order(): + """At most one member is nonzero, whichever it is, so the set says nothing about neighbours.""" + to_spec(varied(UNORDERED, **{'sos.s': {'variable': 'z', 'along': 'bp', 'type': 1}})) + + +@pytest.mark.parametrize(('ordered', 'written'), [(False, {'dtype': 'int'}), (True, {'dtype': 'int', 'ordered': True})]) +def test_a_dimension_writes_ordered_only_where_it_is_true(ordered, written): + """Leaving it out is what `false` says, so a file that never wrote it round-trips unchanged.""" + spec = to_spec({'dimensions': {'t': {'dtype': 'int', 'ordered': ordered}}}) + assert spec.to_dict()['dimensions'] == {'t': written} + assert to_spec(spec.to_dict()) == spec diff --git a/tests/typesetting/golden/model.yaml b/tests/typesetting/golden/model.yaml index fcdce15e5..e97c68d3c 100644 --- a/tests/typesetting/golden/model.yaml +++ b/tests/typesetting/golden/model.yaml @@ -13,13 +13,13 @@ description: >- $5 {net} ~ ^ \ *star* @ref