[table.*]¶
Lookup tables let you define a relationship once — a rating curve, a storage
relationship, a seasonal pattern — and use it from any
dynamic expression. Each table is its own section, and its name
becomes callable through the table.* namespace:
[table.rating]
values = 0, 0,
0.5, 120,
1.2, 450,
3.0, 2200,
[node.gauge_52105]
type = gauge
reference_flow = table.rating(node.reach_5.dsflow)
Tables come in two forms:
- 1D tables interpolate linearly between (x, y) breakpoints.
- 2D tables select a column by exact key match, then interpolate down
that column. With
bilinear = truethey interpolate across both axes.
Tables are global: define one once and reference it from as many expressions
as you like. The [table.*] section can appear anywhere in the model file.
1D tables¶
A 1D table is rows of x, y breakpoints. The x values must be strictly
ascending. Call it with one argument — any expression:
Between breakpoints the value is linearly interpolated. Outside the table range the nearest endpoint value is returned — clamped, never extrapolated. NaN in gives NaN out.
Two things hold exactly, not just to rounding: at a breakpoint the table returns the value written there, and between two breakpoints it never returns a value outside those two. So a table of non-negative values never returns a negative number, however the keys and values round in floating point.
An optional text header row (exactly two non-numeric labels) is allowed for readability:
2D tables¶
A 2D table is a grid. Declare its width with n_cols — the row-key column is
included in the count, so a table with a key column and 12 monthly columns is
n_cols = 13.
The first row is the key row: a non-numeric corner label followed by the column keys. Every following row is a row key and its values. Both key sets must be strictly ascending.
# Pump rate by month (columns) and storage volume (rows)
[table.pump_rating]
n_cols = 4
values = volume\month, 1, 2, 3,
0, 0, 0, 0,
500, 1.0, 1.2, 1.5,
2000, 4.0, 4.8, 6.0,
Call a 2D table with two arguments — the column key first, then the row key:
By default the lookup works in two steps:
- Column selection is an exact match. The first argument must exactly equal one of the column keys. If it doesn't, the simulation stops with an error naming the table, the offending value, and the available keys.
- Row lookup interpolates down the selected column, with the same clamped-linear rule as 1D tables.
Exact match and computed keys
Exact matching is intended for integer-like keys — months, seasons, zone
numbers. sim.month and similar values
match reliably. Avoid computing a column key with arithmetic that can
introduce floating-point error (e.g. volume / 300): a value of
6.9999999… will not match a key of 7, and the run will stop with an
error showing the offending value. For a continuous column key, use
bilinear interpolation.
Bilinear interpolation¶
Set bilinear = true when the column key is a continuous quantity, not a
label, and the table should interpolate between column keys as well as
between row keys:
# Gate discharge by upstream level (columns) and gate opening (rows)
[table.gate_flow]
n_cols = 4
bilinear = true
values = opening\level, 10, 12, 15,
0, 0, 0, 0,
0.5, 20, 30, 45,
1.0, 50, 80, 120,
Kalix interpolates down the two columns either side of the column key, using the row key, then interpolates between those two results using the column key. For a level of 13 and an opening of 0.75: the level-12 column gives 55, the level-15 column gives 82.5, and 13 is a third of the way from 12 to 15, so the result is 64.17.
A bilinear table differs from the default in three ways:
- There is no exact-match error. A column key between two column keys is
interpolated, and one beyond the first or last column key takes that
column. If a missed key should stop the run, leave
bilinearoff. - Both axes clamp. Outside the table on either axis the nearest edge value is returned, never an extrapolation.
- NaN in gives NaN out, for either argument, as for 1D tables.
At a grid point — a column key and a row key that both match — a bilinear
table returns the cell written there, and a column key that matches returns
exactly what the same table returns without bilinear. Between grid points
the result never leaves the range of the four cells around it.
bilinear takes true or false, in lowercase, and defaults to false.
It applies to 2D tables only: bilinear = true on a 1D table is an error at
model load.
The corner label¶
The corner label is text, never a number — that is how the parser recognises
the key row. The convention is y\x, read like the diagonally split corner
of a printed two-way table: y names what runs down the first column, x
names what runs across the top row. It also mirrors the call signature —
table.name(x, y) takes the column key first, the row key second.
Better still, put your real axis names in the corner using the same
rows\columns pattern. Any non-numeric text is accepted, so the corner can
document the table: volume\month in the example above says the rows are
keyed by volume, the columns by month, and (reading it against the call
convention) that the table is called as table.pump_rating(month, volume).
Formatting rules¶
- Line breaks carry no meaning. A table may be written on one line or
spread over any number of indented continuation lines — the same convention
as node tables such as the storage node's
dimensions. - A trailing comma is allowed.
- The number of values must fit the declared shape exactly: an even count for
1D tables, and a multiple of
n_colsfor 2D tables (the corner label makes the key row the same width as every other row). - All cells must be finite numbers —
nanandinfare rejected. - Table names follow the same rules as constants: lowercase letters, digits
and underscores, starting with a letter, no dots, unique per model. Calls
are case-insensitive:
table.Rating(x)andtable.rating(x)are the same.
Composing with expressions¶
Any expression can be a table argument — including another table lookup — and a table call is just a value in the surrounding expression:
[node.dam]
type = storage
demand = max(table.monthly_demand(sim.month, node.dam.volume), const.min_demand)
Editing tables in KalixIDE¶
KalixIDE's Table View (right-click a table's values property, or
Cmd+T / Ctrl+T) opens the table as an editable grid — the same
spreadsheet-style editor used for storage dimensions and loss tables. For 2D
tables the key row appears as the first grid row, so column keys are as
editable as values. The IDE's linter also checks table sections as you type:
shape, key ordering and reference errors are flagged in the editor before the
model ever runs.
When things go wrong¶
| Problem | When detected |
|---|---|
| Malformed values (bad number, wrong count, non-ascending keys) | Model load (and live in the IDE) |
| Unknown table name in an expression | Model load |
| Wrong number of arguments (1D takes 1, 2D takes 2) | Model load |
bilinear = true on a 1D table, or a value other than true or false |
Model load (and live in the IDE) |
2D column key with no exact match (unless the table sets bilinear = true) |
During simulation, with table name, value, and available keys |