Relations#
A relation is a mapping between dimensions: a generator's bus, a snapshot's period, or the buses a generator may connect to. It is declared as a table, and the data supplies its rows.
relations#
dimensions:
bus: { dtype: str }
generator: { dtype: str }
snapshot: { dtype: int }
period: { dtype: int }
relations:
gen_bus: { key: generator, values: bus } # each generator on one bus
period_of: { key: snapshot, values: period }
connection: { key: [generator, bus] } # no values: a generator may connect to several buses
key: and values: each name one dimension or a list of them. The key is
the combination of dimensions that is unique per row: key: generator says
the table has one row per generator. The values are what that row
determines: its bus. With no values:, the key is every column, and the table
is a bare relation.
| Field | ||
|---|---|---|
key |
required. The columns that identify a row | |
values |
the columns the key determines. Omitted, the key is every column | default none |
description |
free text, never parsed | default null |
A column is named after its dimension. Where two columns share a dimension, or
a column maps a dimension onto itself, the mapping form names them:
{bus0: bus, bus1: bus}.
Cardinalities#
| intention | written | cardinality |
|---|---|---|
| each generator has one bus | {key: generator, values: bus} |
many-to-one |
| a bus has several generators | the same table, read the other way | one-to-many |
| a generator may connect to several buses | {key: [generator, bus]}, no values: |
many-to-many |
| a generator has one zone in each period | {key: [generator, period], values: zone} |
many-to-one, keyed by a pair |
| a snapshot has a month, a week and a weekday | {key: snapshot, values: [month, week, weekday]} |
many-to-one, several values |
| a line has two ends, both buses | {key: line, values: {bus0: bus, bus1: bus}} |
many-to-one, two columns over one dimension |
| a snapshot has a representative snapshot | {key: snapshot, values: {rep: snapshot}} |
many-to-one, onto itself |
| a snapshot has neighbours | {key: {from: snapshot, to: snapshot}}, no values: |
many-to-many, onto itself |
| each generator has one bus, and each bus one generator | not a claim the language has | one-to-one |
A call names the column it reads, so one calendar table serves into=month
and within=week alike. A bare table onto itself is summed either way, and
nothing reads a value from it.
A key that determines a value holds one column per dimension, so
{key: {bus0: bus, bus1: bus}, values: line} is refused. A bare relation may
key two columns over one dimension, because nothing reads it.
The data contract#
The data for gen_bus arrives under the key gen_bus, as a table with one
column per declared column, named after it.
- One row per key tuple. A generator on two buses is refused when the data binds.
- Every value is a label of its dimension. A value that matches none is refused as a typo, never added as a member.
- A partial map is the rows it has. A generator in no row sits on no bus, which is absence, as for a parameter.
- A null in any column is refused.
- Row order carries nothing. The order is the dimension's.
How a relation is used#
The declaration fixes no direction. A call names the columns it reads, and a key column named at neither end is joined on: the operand carries its dimension, and the result keeps it. A value column named at neither end is not read. A relation is used in four ways:
| kind | what it does | written as |
|---|---|---|
| aggregate | many rows of the operand collapse onto one | sum(x, by=l, over=a, into=b) |
| read | one row's value becomes a coordinate | at(x, by=l, over=a, into=b) |
| partition | the frame stays, and its rows are grouped | shift, sum_back, position with by=l, within=c |
| test | a row's presence keeps or cuts a coordinate | the relation's name in a where |
Four rules hold for every use:
- A call names every column it reads.
sum(p, by=gen_bus)is refused, so that an edit to the relation never changes what a call means. - Adding a value column is safe. A value column the call does not name is not read, so no call changes its meaning.
- The key is fixed. A key that gains or loses a column re-aims every call that joins on it. Declare a new relation instead.
- An operand may grow. A dimension the relation does not name passes through to the result.
Aggregates and reads#
In both, over= names the columns consumed and into= the columns produced,
and either may be a list. With
zone_of: { key: [generator, period], values: zone } and p over
[generator, period]:
| call | consumes | joins on | produces | result |
|---|---|---|---|---|
sum(p, by=zone_of, over=generator, into=zone) |
generator |
period |
zone |
[zone, period] |
sum(p, by=zone_of, over=period, into=zone) |
period |
generator |
zone |
[generator, zone] |
at(price, by=zone_of, over=zone, into=generator) |
zone |
period |
generator |
[generator, period] |
Two rules more hold for these:
- The result is the operand, less the consumed dimensions, plus the
produced ones. The operand carries every dimension consumed or joined on.
Growing it into a dimension the call lands on is refused: write
load * sum(p, by=gen_bus, over=generator, into=bus), notsum(load * p, ...). A relation onto its own dimension is not this case. sumconsumes at least one key column, andatconsumes value columns only. Either produces any columns. That is what tells a sum from a read: a read finds one row per coordinate, and a sum finds many. Each is refused in the other's case.
over= and into= name different columns, and neither names two columns
over one dimension, since the operand has one axis per dimension.
Partitions#
shift(x, along=d, by=l, within=c), sum_back(x, along=d, by=l, within=c)
and position(d, by=l, within=c) step along the key column over d, join on
the other key columns, and group by the value columns within= names. The
frame does not change. within= is written whenever by= is, so the four
rules above hold. within= may name two columns over one dimension, may not name a key
column, and a bare relation partitions nothing.
Tests#
A where string uses a relation at the frame's own coordinates: it compares a
column under values: against a label, compares two columns of one table, or
tests that a row exists (where strings).