Skip to content
Merged
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
31 changes: 21 additions & 10 deletions docs/reference/language/piecewise.md
Original file line number Diff line number Diff line change
Expand Up @@ -65,7 +65,9 @@ curve, and the [typeset output](../typeset.md) prints the curve itself.
their own, which is the model a consumer that builds rows reads.

A link names the row it writes, so a link may not take a name the block
already writes for itself, such as `convexity` or `lam`.
already writes for itself, such as `convexity` or `lam`. No two blocks may write
the same name: in a file with blocks `a` and `a_b`, a link `b_x` of `a` is
refused, because its row `a_b_x` is also the row of the link `x` of `a_b`.

The breakpoint order is the declared order of `along`. A curve whose breakpoints
decrease in that order is refused when the data binds.
Expand Down Expand Up @@ -222,9 +224,14 @@ writes `at(coupling_lam, by=generator_of, over=generator, into=flow)` into that
row, so the weights stay on `dims:` and the model never names them.

`by:`, `over:` and `into:` are written together. A walk states the relation, the
columns it consumes and the columns it produces, and none is defaulted. A link
whose row is finer than `dims:` is always a walk: a link that names only
`into:` is refused.
columns it consumes and the columns it produces, and none is defaulted. Each of
`over:` and `into:` names at least one column. A link whose row is finer than
`dims:` is always a walk: a link that names only `into:` is refused.

A walk is held to every rule of `at`, as the model loads, and a refusal names
the link. `into:` names key columns of the relation, and the read has one value
at each coordinate it lands on. A key column that the walk does not name is
joined on, so its dimension is one of `dims:`.

A block whose only link walks a relation is a curve. Two links is what a curve
needs when a link is one row; a walked link is one row per fine coordinate, so
Expand All @@ -250,17 +257,21 @@ The `power` row is built where
`at(has_curve, by=generator_of, over=generator, into=flow)` holds, which is at
every flow of a generator with a curve. The values of a walked link are asked
for at the same rows, so a flow of a generator with no curve needs no row in
`bp_power`. A mask over dimensions the walk keeps, such as `snapshot` alone,
reaches the row as written. A mask that carries some of the dimensions the walk
reads through and not the others is refused, and the message names the ones
missing.
`bp_power`. A mask that carries no dimension the walk consumes, such as
`snapshot` alone, reaches the row as written. This is also true when the
relation is keyed on `snapshot` too, because the row keeps every dimension the
walk joins on. A mask that carries a dimension the walk consumes and not every
dimension the walk joins on is refused, and the message names the ones missing.
A mask over a dimension a walk produces, such as `flow`, is refused: the mask
says which curves exist, and there is one curve per coordinate of `dims:`.

| A walked link | |
| ------------- | ---------------------------------------------------------------------------------------------------------------------------------------------------------- |
| _over_ | names a column over a dimension of `dims:` |
| _into_ | names a column over a dimension that `dims:` does not carry, and that is not `along` |
| _into_ | names key columns over dimensions that `dims:` does not carry, and that are not `along` |
| _by_ | a relation whose other key columns are over dimensions of `dims:` |
| _values_ | follows the **link's** row: `bp_power` is per flow, not per generator |
| `where:` | on the block reaches the link's row read through the relation, or as written where the mask carries none of the dimensions the walk reads through |
| `where:` | on the block reaches the link's row read through the relation, or as written where the mask carries none of the dimensions the walk consumes |
| `method:` | `adjacency` or `sos2`. `lp` loses the abscissa its segment line is written against, and `convex` loses the pair of values parameters it reads a shape from |

### Signs
Expand Down
4 changes: 2 additions & 2 deletions src/math_spec/_expression_resolver.py
Original file line number Diff line number Diff line change
Expand Up @@ -502,7 +502,7 @@ def relation_ref(
return self.partition(name, operator, along, named['within'])
if not ({'over', 'into'} <= set(named)):
return None # refused already, by the call shape or by the role that named no column
return self._direction(name, operator, named['over'], named['into'])
return self.direction(name, operator, named['over'], named['into'])

def _role_name(self, value: ArithmeticNode, operator: str, key: str) -> tuple[str, ...] | None:
"""``over=`` or ``into=`` as the column names it must be — one bare name, or a bracketed list of them."""
Expand All @@ -513,7 +513,7 @@ def _role_name(self, value: ArithmeticNode, operator: str, key: str) -> tuple[st
)
return None

def _direction(
def direction(
self,
name: str,
operator: str,
Expand Down
17 changes: 9 additions & 8 deletions src/math_spec/lowering.py
Original file line number Diff line number Diff line change
Expand Up @@ -20,7 +20,7 @@
from math_spec.dimensions import check_schema, dims_of
from math_spec.errors import SchemaError, prefixed
from math_spec.expansion import expand, parse_template
from math_spec.piecewise import assumptions_of, declaration_of, lp_domain_refusal, resolve_links
from math_spec.piecewise import assumptions_of, declaration_of, lp_domain_refusal, resolve_links, resolve_walks
from math_spec.program import (
Assumption,
BooleanLiteral,
Expand Down Expand Up @@ -52,7 +52,7 @@

if TYPE_CHECKING:
from math_spec.model import AssumptionBlock, Spec
from math_spec.program import Expression
from math_spec.program import Direction, Expression


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

curves: dict[str, tuple[tuple[Expression, ...], Mask | None]] = {}
curves: dict[str, tuple[tuple[Expression, ...], dict[str, Direction], Mask | 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))
if links is None:
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, where)
curves[pname] = (links, walks, where)

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)
in_math = frozenset(node.name for node in walk(*roots) if isinstance(node, Named))

piecewise = {}
for pname, (links, where) in curves.items():
for pname, (links, walks, where) in curves.items():
pdef = schema.piecewise[pname]
piecewise[pname] = declaration_of(schema, pname, pdef, links, where)
piecewise[pname] = declaration_of(schema, pname, pdef, links, walks, where)
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
6 changes: 6 additions & 0 deletions src/math_spec/model.py
Original file line number Diff line number Diff line change
Expand Up @@ -539,6 +539,12 @@ def _check_walk(self) -> PiecewiseLink:
f'consumes and the columns it produces, as at() does; none is defaulted.'
)
raise ValueError(msg)
if empty := [k for k in ('over', 'into') if written[k] == []]:
msg = (
f'{empty[0]}: [] names no column — a walk consumes at least one column of the relation and produces '
f'at least one. Name a column, or a list of them.'
)
raise ValueError(msg)
return self

@model_validator(mode='before')
Expand Down
Loading
Loading