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
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 | default null |
A column is named after its dimension. Where two columns share a dimension, 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 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.
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.
- A partial map is the rows it has. A generator in no row sits on no bus, which is absence.
- 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 joins the operand to the relation on the columns they share, and groups what the join produces. The call names the columns that decide both.
| kind | what it does | written as |
|---|---|---|
| join and group by | rows of the operand match rows of the relation, and each group is added up | sum(x, by=l, over=a, into=b) |
| join | each row of the operand matches one row of the relation, and reads its value | 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. - A value column the call does not name is not read. So adding one to the relation changes no call.
- The key is fixed. To change it, declare a new relation.
- A dimension the relation does not name passes through to the result.
Joins and group-bys#
Each column of the relation is either joined on or not, and either
grouped by or not. over= names the columns joined on and not grouped by.
into= names the columns grouped by and not joined on. Either may be a list.
| column of the relation | joined on | grouped by | in the result |
|---|---|---|---|
over= |
yes | no | leaves |
into= |
no | yes | arrives |
| a key column the call omits | yes | yes | stays |
| a value column the call omits | no | no | is not read |
With zone_of: { key: [generator, period], values: zone }, the bare
connection: { key: [generator, bus] } and p over [generator, period]:
| call | joins on | groups by | result |
|---|---|---|---|
sum(p, by=zone_of, over=generator, into=zone) |
generator, period |
zone, period |
[zone, period] |
sum(p, by=zone_of, over=period, into=zone) |
period, generator |
zone, generator |
[generator, zone] |
sum(p, by=connection, over=generator, into=bus) |
generator |
bus |
[bus, period] |
at(price, by=zone_of, over=zone, into=generator) |
zone, period |
generator, period |
[generator, period] |
- The result is the operand, less the columns joined on, plus the columns
grouped by. The operand carries every column joined on, and none that the
call groups by: a column the operand carries would be joined on, not grouped
by.
sum(load * p, by=gen_bus, over=generator, into=bus)is refused; writeload * sum(p, by=gen_bus, over=generator, into=bus). - A sum adds up several rows per group, and a read finds one. Where the
columns grouped by hold the whole key, every group is one row. That is a join
with no group-by, which is
at. Asumthere is refused towardat, and anatwhose groups hold several rows is refused towardsum:
Constraint 'cap': sum(by=zone_of): the columns this sum groups by, ['generator', 'period'], hold the whole key ['generator', 'period'], so every group is one row and nothing is added up — that is a join with no group-by, which is at()'s. Write at(..., by=zone_of, over=['zone'], into=['generator']), or sum toward a value column.
- A sum joins on at least one key column, and groups by any column it does
not join on. Either end may name a value column beside a key one.
connectionhas only key columns, and the sum above joins on one and groups by the other. - A read joins on value columns, and groups by the key. Its result carries
every key column, named in
into=or left unnamed, and whatever else the operand carries that the read does not join on. over=andinto=name different columns, and neither names two columns over one 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 partition the rows by the value columns within=
names. The
frame does not change. within= is written whenever by= is. It 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).