Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
1 change: 1 addition & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -12,6 +12,7 @@ it releases that version ([RELEASING.md](https://github.com/energy-models/mathsp

## Upcoming version

- feat(language): a piecewise gate reads its binary through a relation, as a link walks one ([#784](https://github.com/energy-models/mathspec/pull/784))
- feat(language): a piecewise block states its dims and names its links, and its where reaches links that walk a relation ([#630](https://github.com/energy-models/mathspec/pull/630))
- feat(language): a parameter value that is null or NaN is refused when the data is attached ([#788](https://github.com/energy-models/mathspec/pull/788))
- fix(language): a where string names several columns in at's over= and into=, as an expression does ([#782](https://github.com/energy-models/mathspec/pull/782))
Expand Down
71 changes: 59 additions & 12 deletions docs/reference/language/piecewise.md
Original file line number Diff line number Diff line change
Expand Up @@ -26,7 +26,7 @@ piecewise:
fuel: [fuel, fuel_bp]
heat: [heat, heat_bp]
method: adjacency # how the weights are restricted — below
activity: null # optional: a binary variable that the weights sum to
activity: null # optional: a binary variable that the weights sum to, or a walk to one

# a link may be bounded by the curve instead of pinned to it
fuel_cap:
Expand All @@ -45,14 +45,14 @@ piecewise:
| _sign_ | `<=` or `>=`. It bounds the link by the curve instead of pinning it to it. Any number of links may carry one, as long as at least one link does not ([below](#signs)) |
| _by_, _over_, _into_ | A relation walk from the curve's `dims:` to the link's row ([below](#a-link-that-walks-a-relation)) |

| Key | | |
| ---------- | ---------------------------------------------------------------------------------------- | ------------------- |
| `along` | required. The dimension each curve runs along | |
| `dims` | required. The dimensions the block builds one curve per coordinate of ([below](#dims)) | |
| `links` | required. Two or more links, or one that walks a relation | |
| `where` | which coordinates have a curve, and how far each runs ([below](#where)) | default `null` |
| `method` | `adjacency`, `sos2`, `convex` or `lp`: how the weights are restricted ([below](#method)) | default `adjacency` |
| `activity` | a binary variable that gates the curve ([below](#activity)) | default `null` |
| Key | | |
| ---------- | --------------------------------------------------------------------------------------------- | ------------------- |
| `along` | required. The dimension each curve runs along | |
| `dims` | required. The dimensions the block builds one curve per coordinate of ([below](#dims)) | |
| `links` | required. Two or more links, or one that walks a relation | |
| `where` | which coordinates have a curve, and how far each runs ([below](#where)) | default `null` |
| `method` | `adjacency`, `sos2`, `convex` or `lp`: how the weights are restricted ([below](#method)) | default `adjacency` |
| `activity` | a binary variable that gates the curve, on `dims:` or through a relation ([below](#activity)) | default `null` |

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.
Expand Down Expand Up @@ -84,9 +84,10 @@ period read off a curve that has none, is said by adding that dimension to
vary along it is the data's business: values that do not carry it give one curve
shape and a per-period operating point.

An [`activity:`](#activity) gate carries no dimension that `dims:` does not. A
gate over fewer dimensions switches every curve it covers: a gate per generator
switches that generator's curve in every snapshot.
An [`activity:`](#activity) gate carries no dimension that `dims:` does not,
or [walks a relation](#a-gate-that-walks-a-relation) onto them. A gate over
fewer dimensions switches every curve it covers: a gate per generator switches
that generator's curve in every snapshot.

### `where`

Expand Down Expand Up @@ -168,6 +169,52 @@ Where the gate does not exist, the curve is ungated. To pin the curve off
there instead, put `absence: zero` on the gate. To build no curve there at all,
use [`where:`](#where).

#### A gate that walks a relation

A gate whose binary is over another dimension reads it through a
[relation](relations.md#how-a-relation-is-used), as [`at`](operators.md#at)
does. Write the gate as a mapping with `variable:`, `by:`, `over:` and `into:`.
Here the on/off binary is per status entity, and each converter's curve reads
the status of its entity:

```yaml
dimensions:
converter: { dtype: str }
status_entity: { dtype: str }
snapshot: { dtype: int }
bp: { dtype: int }
relations:
pw_status_of: { key: converter, values: status_entity }
parameters:
bp_p: { dims: [converter, bp] }
bp_fuel: { dims: [converter, bp] }
variables:
running: { dims: [status_entity, snapshot], domain: binary }
p: { dims: [converter, snapshot] }
fuel: { dims: [converter, snapshot] }
piecewise:
curve:
along: bp
dims: [converter, snapshot]
links:
p: [p, bp_p]
fuel: [fuel, bp_fuel]
method: sos2
activity: { variable: running, by: pw_status_of, over: status_entity, into: converter }
objective:
sense: minimize
expression: sum(fuel)
```

The weights of each curve sum to
`at(running, by=pw_status_of, over=status_entity, into=converter)`. A converter
with no row in `pw_status_of` has no status, and its curve is ungated. Under
`absence: zero` on the gate, a converter whose entity is off the gate's mask is
pinned off, and only a missing row ungates a curve.

The walk is held to the rules of `at`. The gate lands on dimensions of `dims:`,
and a gate that lands on another dimension is refused.

### A link that walks a relation

A link that names `by:`, `over:` and `into:` reads the curve's weights through a
Expand Down
75 changes: 72 additions & 3 deletions schema/mathspec.schema.json
Original file line number Diff line number Diff line change
Expand Up @@ -588,21 +588,90 @@
"title": "ParameterBlock",
"type": "object"
},
"PiecewiseActivity": {
"anyOf": [
{
"additionalProperties": false,
"description": "A piecewise block's gate: the binary the weights sum to, on the block's own dims or read through a relation.\n\nWritten in YAML as the variable's name, and serialised back to it. A gate\nthat names ``by:``, ``over:`` and ``into:`` is written as a mapping, and\nreads the binary through the relation as ``at`` reads it, so a unit's\non/off variable over its own dimension switches the curves it maps to. A\ncurve whose coordinate has no row in the relation is ungated.",
"properties": {
"by": {
"anyOf": [
{
"type": "string"
},
{
"type": "null"
}
],
"default": null,
"title": "By"
},
"into": {
"anyOf": [
{
"type": "string"
},
{
"items": {
"type": "string"
},
"type": "array"
},
{
"type": "null"
}
],
"default": null,
"title": "Into"
},
"over": {
"anyOf": [
{
"type": "string"
},
{
"items": {
"type": "string"
},
"type": "array"
},
{
"type": "null"
}
],
"default": null,
"title": "Over"
},
"variable": {
"title": "Variable",
"type": "string"
}
},
"required": [
"variable"
],
"title": "PiecewiseActivity",
"type": "object"
},
{
"type": "string"
}
]
},
"PiecewiseBlock": {
"additionalProperties": false,
"description": "Expressions tied to one breakpoint-indexed piecewise curve per coordinate of ``dims:``.\n\nMirrors ``linopy.Spec.add_piecewise_formulation``. ``links:`` maps a name\nto ``[expression, values_parameter]`` or ``[expression, values_parameter,\nsign]``: *expression* is any affine expression string over ``dims:``,\n*values_parameter* names a parameter carrying ``along`` and no dim the\nlink's row lacks, and *sign* bounds the link by the curve instead of\npinning it. The name is the\nlink's row in the expansion, ``<block>_<link>``.\n\n``dims:`` alone decides how many curves the block builds: one set of\nweights per coordinate of it. ``where:`` says which of those coordinates\nhave a curve, and how far each runs along ``along`` where it reads that\ndim too; ``activity:`` whether a curve that exists is switched on. A link\nthat walks a relation builds a row per fine coordinate, each reading the\none curve its coarse coordinate has, so how many expressions a curve ties\nis data.",
"properties": {
"activity": {
"anyOf": [
{
"type": "string"
"$ref": "#/$defs/PiecewiseActivity"
},
{
"type": "null"
}
],
"default": null,
"title": "Activity"
"default": null
},
"along": {
"title": "Along",
Expand Down
26 changes: 19 additions & 7 deletions src/mathspec/lowering.py
Original file line number Diff line number Diff line change
Expand Up @@ -20,7 +20,14 @@
from mathspec.dimensions import check_schema, dims_of
from mathspec.errors import SchemaError, did_you_mean, prefixed
from mathspec.expansion import expand, parse_template
from mathspec.piecewise import assumptions_of, declaration_of, lp_domain_refusal, resolve_links, resolve_walks
from mathspec.piecewise import (
assumptions_of,
declaration_of,
lp_domain_refusal,
resolve_gate,
resolve_links,
resolve_walks,
)
from mathspec.program import (
Assumption,
BooleanLiteral,
Expand Down Expand Up @@ -56,7 +63,7 @@
if TYPE_CHECKING:
from collections.abc import Mapping

from mathspec.program import Direction, Expression
from mathspec.program import Direction, Expression, Gate
from mathspec.spec import AssumptionBlock, Spec


Expand Down Expand Up @@ -195,31 +202,36 @@ def lower(schema: Spec) -> Program:
if (assumption := _assumption(aname, adef, ns, errors)) is not None:
assumptions[aname] = assumption

curves: dict[str, tuple[tuple[Expression, ...], dict[str, Direction], Mask | None]] = {}
curves: dict[str, tuple[tuple[Expression, ...], dict[str, Direction], Mask | None, Gate | None]] = {}
for pname, pdef in schema.piecewise.items():
links = resolve_links(pname, pdef, ns, errors)
walks = resolve_walks(pname, pdef, ns, errors)
where = mask_of(resolve_where_text(pdef.where, ns, f"piecewise '{pname}' where", errors))
gate = None
if pdef.activity is not None:
gate = resolve_gate(pname, pdef, variables[pdef.activity.variable], ns, errors)
if gate is None:
continue
if links is None or walks is None:
continue
if pdef.method == 'lp' and (refusal := lp_domain_refusal(pname, pdef, links)) is not None:
errors.append(refusal)
curves[pname] = (links, walks, where)
curves[pname] = (links, walks, where, gate)

if errors:
raise SchemaError('\n'.join(errors))

roots = [side for c in constraints.values() for side in (c.lhs, c.rhs)]
if objective is not None:
roots.append(objective.expression)
roots.extend(link for links, _, _ in curves.values() for link in links)
roots.extend(link for links, *_ in curves.values() for link in links)
roots.extend(terms.values())
in_math = frozenset(node.name for node in walk(*roots) if isinstance(node, Named))

piecewise = {}
for pname, (links, walks, where) in curves.items():
for pname, (links, walks, where, gate) in curves.items():
pdef = schema.piecewise[pname]
piecewise[pname] = declaration_of(schema, pname, pdef, links, walks, where)
piecewise[pname] = declaration_of(schema, pname, pdef, links, walks, where, gate)
for aname, assumed in assumptions_of(pname, piecewise[pname], pdef.where).items():
assumption = _assumption(aname, assumed, ns, errors)
assert assumption is not None and not errors, 'what a method assumes is stated in the language'
Expand Down
Loading
Loading