A transformation is FEWS doing arithmetic on data it already holds. It reads one
or more inputtime series, computes
something — gap-fill, aggregate, convert stage to flow — and writes one or more
output series back into the data store. That’s hop
④, the process step: no outside world, no ID mapping,
just internal series in and internal series out. In a real run it’s almost always
a middle step — the pre-processing before a model and the post-processing
after it, both listed in a workflow (see the
lifecycle, steps 3 and 5).
Strip away the dozens of transformation types and every one has the same shape:
a transformation names a function, the function reads an
inputVariable and writes an outputVariable, and each of those is just
a timeSeriesSet (the same kind you write in an import). Compute in the middle,
a series on each side.
transformationModule (version="1.0")
├─ variable* ← optional named timeSeriesSets, reusable across steps
└─ transformation id="…" (one or more, run top to bottom)
└─ <functionType> ← e.g. interpolationSerial, aggregation, stageDischarge…
└─ <variant> ← the specific function (e.g. linear, accumulative)
├─ inputVariable ─┐ each is EITHER
│ └─ timeSeriesSet │ an embedded <timeSeriesSet>
└─ outputVariable ─┘ OR a <variableId> ref to a <variable>
└─ timeSeriesSet
Two things carry the whole model:
Input and output are ordinary timeSeriesSets. The function reads the
series the input set resolves to and writes the series the output set
describes — same fields (moduleInstanceId, parameterId, locationId,
timeSeriesType, timeStep) you already know from
importing.
Transformations chain by naming. One step’s output series is the next
step’s input. Define it once as a <variable> and reference it by
variableId, or just write the same timeSeriesSet on both sides of the
seam.
Imported gauge data has holes — a sensor dropout, a late telemetry packet. Before
a model consumes Q.obs it usually wants a continuous series. A serial linear
interpolation fills short gaps by drawing a straight line between the values on
either side, and writes the result as a new, derived series.
inputVariable points at exactly the series the
import wrote — same header, external historical,
from moduleInstanceIdImportObservations. If any field here doesn’t match a
stored series, the transformation reads nothing.
maxGapLength (optional) caps how big a gap gets filled — here, gaps of up
to 6 time steps. Larger holes are left missing rather than invented across.
outputVariable describes the new series. Note moduleInstanceId is now
PreProcessObservations and timeSeriesType is simulated historical: this
value was computed by FEWS, not observed, so it gets a distinct header and
won’t collide with the raw import.
id="FillGapsQobs" on transformation is a label for logs and error
messages only — it is not the series’ identity.
A transformation is defined by what it reads and writes, not by named
locations baked into the function. Because the input and output are
timeSeriesSets, everything you can do with a set applies:
Swap locationId for locationSetId (e.g. AllDischargeGauges) and the
one transformation runs per location in the set — gap-fill every gauge with a
single block. FEWS pairs each input location with the same-named output
location.
The output’s parameterId and timeStep can differ from the input’s.
That’s exactly how an aggregation works: read hourly Q.obs, write a daily
series with timeStep unit="day".
Chaining steps. List several transformation elements; they run top to
bottom. Give an intermediate result a timeSeriesType of temporary (or
hold it in a <variable>) when it exists only to feed the next step and
needn’t persist.
Reusable variables. Declare a <variable> with a variableId and a
timeSeriesSet at the top of the module, then reference it by <variableId>
in later inputVariable/outputVariable blocks instead of repeating the set.
Pre- vs post-processing. The same module type does both — placement in
the workflow decides. Gap-fill and rating curves run
before the model; ensemble stats and error
correction run after.
Different result per range or period. Instead of a single function, a
transformation can hold rangeTransformations (a different computation for
different value ranges of a limit variable) or periodTransformations
(different by time period) — reach for these when one formula doesn’t fit the
whole domain.
The function element (interpolationSerial above) is one of many. You don’t
learn them all up front; you reach for the family that matches the job. The
common ones:
Family
Root element(s)
Typical use
Gap-filling / interpolation
interpolationSerial, interpolationSpatial
Fill holes, extrapolate, interpolate across gauges.
Reads nothing → the input header doesn’t match. The inputVariable’s
timeSeriesSet must reconstruct the exact header an earlier hop stored —
moduleInstanceId, parameterId, locationId, timeSeriesType, timeStep,
field for field. A mismatch isn’t an error; the transformation just has no
data to work on. Line it up against the producing import
or upstream step.
Output overwrites the input → give the result its own identity. If the
output timeSeriesSet has the same header as the input, the derived series
lands on top of the source. Change the moduleInstanceId and use a computed
timeSeriesType (simulated historical / simulated forecasting) so the
result is a distinct series.
readWriteMode is required, but inert here. The schema requires it on
every timeSeriesSet, so include it (read only on inputs, add originals on
outputs is a sensible convention). Its read-only-vs-editable semantics are
honoured only by the Time Series Display; the transformation module ignores
the value, so don’t rely on it to control writes.
Runs but nothing downstream sees it. Whatever consumes the result — the
model, a filter, a
threshold check — must query the output header you
wrote, not the input one. Match the two ends of every seam.
A change has no effect → a higher-versioned file wins. Check for a sibling
like PreProcessObservations 1.01 default.xml. See how FEWS finds a
file.
Run the transformation as a step in a workflow and
read the log. Config problems and “no input data” show up here, keyed by the
transformationid.
Browse the derived series. Add a filter whose
timeSeriesSet matches the output header (here: moduleInstanceIdPreProcessObservations, simulated historical) and confirm it appears.
Compare input and output on one chart. Plot the raw Q.obs and the
gap-filled series together — the fills should bridge exactly the holes you
expected, and no farther than maxGapLength.