Tabular Sink
A durable TransitionSink: one flat table of transitions, written through KSL's TabularOutputFile, plus the provenance that makes the table readable by something that has never seen the model.
Two files, and the second one is not optional
A transition is positional. a_Mode = 2.0 means nothing without the declaration saying that lever is CATEGORICAL over ["slow", "normal", "fast"], that it is a SETTING rather than a TRANSACTION — which is a fact about the dynamics, not decoration — and where its bounds are. Naming columns from the descriptor carries position-to-name and nothing else. So this sink writes both:
<name>.sqlite— the rows. It is an ordinary SQLite database (that is what aTabularOutputFileis), so it is queryable with any SQL tool and readable from Python.<name>.provenance.json— the RunProvenance: model, experiment, element, policy label, and the full descriptor.
TrajectoryFile pairs them on read and refuses a trajectory whose provenance is missing, rather than guessing. Guessing is how a categorical index quietly becomes a continuous one.
Column names are sanitised, and that is not defensive
TabularOutputFile interpolates column names straight into CREATE TABLE. Measured: a : or a - in a name fails the create outright, and a space is worse — SQLite parses s_L Queue as a column s_L of type Queue, so the file is created with a silently wrong column. KSL element names are colon-qualified by convention (Room:Position), so every realistic name hits this. Names are therefore reduced to [A-Za-z0-9_], and a collision produced by that reduction is refused at construction with both original names in the message.
The schema
For n observations and m levers: 10 + 2n + 3m columns.
| Column | Type | Meaning |
|---|---|---|
element, rep, epoch | TEXT, NUMERIC, NUMERIC | which element, replication and decision |
time, tau | NUMERIC | the successor's time, and the interval this row covers |
s_*, sp_* | NUMERIC | state and successor state, one column per declared observation |
a_* | NUMERIC | the action applied |
p_* | NUMERIC | the action proposed. Always written; equal to a_* when nothing was repaired |
repaired | NUMERIC | 1 when the rule's request was repaired, so p_* differs from a_* |
unavail_* | NUMERIC | 1 when that lever's feasible set was empty and it took its neutral |
reward | NUMERIC | already sign-normalised: larger is better, because COST is negated once at declaration |
terminated, truncated | NUMERIC | 0/1, and deliberately separate — a learner that bootstraps from the last row needs to know which it got |
source | TEXT | why the episode ended; "" when it had not |
Nulls are encoded rather than stored. Row.setNumeric takes a non-null Double and DataType has only NUMERIC and TEXT, so the three nullable fields of a TransitionRecord need a convention. proposedAction and leverUnavailable are always written, with repaired saying whether the proposal is news; source is "" for absent. NaN was rejected as the null marker: a NaN in training data is a trap that surfaces three steps later inside somebody's learner.