IO Binding¶
IO binding connects your components to Entry fields. A model with a forward(image) parameter can read from entry["satellite_rgb"] in one experiment and entry["camera_input"] in another — same model, different data.
This page is the comprehensive reference for all binding syntax. For concept guides, see: Model | DataTransform | EntryTransform | SequentialModel
The Big Idea, Step by Step¶
If you've never used a routing layer like this before, here's the mental model in five short steps. Skip ahead if you already grok it.
1. The problem: rigid coupling¶
A naive model signature couples the parameter name to a specific data field:
Now this model only works on entry["satellite_rgb"]. Plug it into a different dataset (say one with entry["camera_input"]) and you have to rewrite the model — or rewrite the dataset to use the model's preferred name. Neither scales.
2. The two-name idea¶
Decouple them. Give the component two layers of naming:
- Parameter names — what the component's code uses internally. Stable across experiments.
- Field names — what's in
Entry. Specific to whatever dataset is loaded.
A small routing layer between them maps "the value behind this parameter name" to "the value at this Entry field name." That's IO binding.
3. What that looks like in practice¶
The same Upscaler with both names made explicit:
class Upscaler(Model):
def forward(self, image): # ← parameter name
return self.net(image)
model = Upscaler()
model.set_io({"inputs": {"image": "satellite_rgb"}}) # ← routing
Read this as: "When you need image, read it from entry["satellite_rgb"]."
In a different experiment, the same model class with different routing:
The model's code doesn't change. Only the routing does.
4. Reading the syntax¶
The routing dict is always:
inputsmaps parameter names (LHS) to Entry field names (RHS).outputssays where to write results (a string or list of strings — for Models, where the prediction goes; some components don't have outputs and can omit it).
The LHS (parameter name) is determined by your component's code — you can't change it without editing the code. The RHS (field name) is determined by your data / experiment — you change it via YAML.
5. Where the routing lives in YAML¶
In a config, the routing goes under an io: block next to the component's _target + params:
model:
_target: my_models.Upscaler
params:
channels: 64
io:
inputs: {image: satellite_rgb} # ← routing for this run
outputs: prediction
Want a different routing? Change the YAML, not the Python.
That's the entire concept. The rest of this page is the syntax reference for each component type — Model, DataTransform, EntryTransform, Loss, SequentialModel — and the edge cases (multiple applications, dict-valued fields, nested wrappers).
All Binding Methods¶
| Method | Where | When to Use |
|---|---|---|
set_io(...) |
Python code | Programmatic setup, tests |
io: key in YAML |
Config files | Config-driven instantiation |
Flow DSL: x -> module -> y |
SequentialModel | Multi-stage pipelines |
The One Format¶
Model, DataTransform, and Loss use the same set_io() format. EntryTransform uses constructor _key params instead (see below).
inputs: A dict mapping method parameter names to Entry field names, or a list of such dicts for multiple applications.outputs: A string, list, or omitted. Determines where results are written.
When can you omit outputs?
Only for DataTransform with a single input that returns a single value. The result is written back to the input field (in-place). If there are multiple inputs or multiple return values, outputs is required. Model always requires outputs.
Model¶
Model inputs are inferred from forward() parameter names. Output field names come from the outputs key.
set_io() in Python¶
Single output:
class Upscaler(Model):
def forward(self, image):
return self.net(image)
model = Upscaler()
model.set_io({"inputs": {"image": "image_rgb"}, "outputs": "prediction"})
# "image" matches forward() param → reads entry["image_rgb"]
# "prediction" → writes entry["prediction"]
YAML io: Config¶
Multiple Inputs¶
Models with multiple forward() parameters:
class FusionModel(Model):
def forward(self, image, guide):
return self.net(image, guide)
model.set_io({
"inputs": {"image": "x", "guide": "edges"},
"outputs": "result",
})
Multiple Outputs¶
When forward() returns multiple values, use a list of output names or positional mapping in the flow DSL:
With set_io() — list of output field names:
class FusionModel(Model):
def forward(self, image, guide):
result, attn = self.net(image, guide)
return result, attn
model = FusionModel()
model.set_io({
"inputs": {"image": "x", "guide": "edges"},
"outputs": ["result", "attn"],
})
# 1st return value → entry["result"]
# 2nd return value → entry["attn"]
With flow DSL — positional mapping:
flow = ["(x, edges) -> fuse -> (result, attn)"]
# 1st return value → entry["result"]
# 2nd return value → entry["attn"]
Optional Inputs¶
Parameters with defaults in forward() are optional — they can be omitted from the IO config, and the default value applies:
class MyModel(Model):
def forward(self, image, guide=None):
if guide is not None:
return self.guided_net(image, guide)
return self.net(image)
# OK — guide omitted from inputs, default (None) applies
model.set_io({"inputs": {"image": "x"}, "outputs": "y"})
result = model(Entry(x=tensor)) # guide=None
# OK — guide mapped and field exists
model.set_io({"inputs": {"image": "x", "guide": "edges"}, "outputs": "y"})
result = model(Entry(x=tensor, edges=edge_tensor)) # guide=edge_tensor
# FAILS — guide mapped but field missing
model.set_io({"inputs": {"image": "x", "guide": "edges"}, "outputs": "y"})
result = model(Entry(x=tensor)) # KeyError: field 'edges' not found
The rule is the same for both Model and DataTransform: if you map it, it must exist.
SequentialModel Flow DSL¶
| Syntax | Example | Notes |
|---|---|---|
| Single input/output | x -> m1 -> y |
Inputs by signature order |
| Multi-input | (x, edges) -> m1 -> y |
Positional by signature order |
| Multi-output | (x, g) -> m1 -> (result, conf) |
Positional return values |
| Named inputs | (image=x, guide=edges) -> m1 -> y |
Explicit parameter mapping |
| Reuse | x -> enc -> h1 then h1 -> enc -> h2 |
Shared weights, different fields |
Models are fully reusable — the same instance can appear in multiple flow lines with shared weights. Each flow line provides its own IO mapping.
DataTransform¶
DataTransform inputs are inferred from transform() or transform_unbatched() parameter names. Type annotations control recursion into containers.
Single-Parameter Transforms¶
For transforms with one parameter (e.g., transform(self, image: Tensor)):
In-place — transform and write back to the same field:
With new output name:
t.set_io({"inputs": {"image": "x"}, "outputs": "x_scaled"})
# Reads entry["x"], writes to entry["x_scaled"]
Multiple fields — apply the same transform to each field independently:
t.set_io({"inputs": [{"image": "x"}, {"image": "y"}]})
# In-place on both: entry["x"] and entry["y"]
t.set_io({
"inputs": [{"image": "x"}, {"image": "y"}],
"outputs": ["x_scaled", "y_scaled"],
})
# Reads entry["x"] → writes entry["x_scaled"]
# Reads entry["y"] → writes entry["y_scaled"]
YAML equivalents:
# In-place on single field
io:
inputs:
image: x
# With new output name
io:
inputs:
image: x
outputs: x_scaled
# Multiple fields, in-place
io:
inputs:
- {image: x}
- {image: y}
# Multiple fields with different output names
io:
inputs: [{image: x}, {image: y}]
outputs: [x_scaled, y_scaled]
Multi-Parameter Transforms¶
When transform() takes multiple parameters, map each to an Entry field:
# MatchReference: transform(self, image: Tensor, reference: Tensor)
t.set_io({
"inputs": {"image": "lrs", "reference": "hr"},
"outputs": "matched",
})
Multiple Outputs¶
When transform() returns a tuple, map each value to a field using a list of output names:
class SplitChannels(DataTransform):
def transform(self, image: torch.Tensor):
return image[:, :3], image[:, 3:]
t = SplitChannels()
t.set_io({"inputs": {"image": "x"}, "outputs": ["rgb", "extra"]})
Multiple Applications¶
Apply the same transform to different fields independently:
# Single-parameter, multiple fields
t.set_io({"inputs": [{"image": "x"}, {"image": "y"}]}) # In-place on both
# Multi-parameter, multiple applications
t.set_io({
"inputs": [
{"image": "a1", "reference": "b1"},
{"image": "a2", "reference": "b2"},
],
"outputs": ["o1", "o2"],
})
# Application 1: transform(image=entry["a1"], reference=entry["b1"]) → entry["o1"]
# Application 2: transform(image=entry["a2"], reference=entry["b2"]) → entry["o2"]
Multiple applications with multiple outputs per application:
t.set_io({
"inputs": [
{"image": "a1", "reference": "b1"},
{"image": "a2", "reference": "b2"},
],
"outputs": [["o1a", "o1b"], ["o2a", "o2b"]],
})
In-Place Transforms¶
DataTransforms can overwrite the source field — unlike Models, which cannot overwrite existing Entry fields:
In SequentialModel:
Optional Parameters¶
Parameters with defaults in transform() are optional — they can be omitted from the IO config:
class AddNoise(DataTransform):
def transform(self, image: Tensor, mask: Tensor = None):
...
noise = AddNoise()
# OK — mask omitted, default (None) applies
noise.set_io({"inputs": {"image": "x"}, "outputs": "noisy"})
# OK — mask mapped and field exists
noise.set_io({"inputs": {"image": "x", "mask": "m"}, "outputs": "noisy"})
# FAILS — mask mapped but field missing → KeyError
noise.set_io({"inputs": {"image": "x", "mask": "m"}, "outputs": "noisy"})
noise(Entry(x=tensor)) # KeyError: field 'm' not found
SequentialModel Flow DSL¶
| Syntax | Example | Effect |
|---|---|---|
| Single input/output | x -> mul -> y |
Inputs by signature order |
| Multi-input | (x, ref) -> t -> y |
Positional by signature order |
| Multi-output | x -> t -> (a, b) |
Positional return values |
| Named inputs | (image=x, reference=ref) -> t -> out |
Explicit parameter mapping |
| In-place | x -> mul -> x |
Overwrites entry["x"] |
| Reuse | x -> mul -> x2 then y -> mul -> y2 |
Same instance, different fields |
DataTransforms are fully reusable — the same instance can appear in multiple flow lines. No instance state is mutated; applications are computed externally per step.
Model vs DataTransform¶
Model and DataTransform share the same IO binding interface. The table below highlights what's identical and where they differ.
| Feature | Model | DataTransform |
|---|---|---|
set_io() format |
{"inputs": {...}, "outputs": ...} |
Same |
YAML io: key |
Same | Same |
| Flow DSL (positional) | x -> m -> y |
Same |
| Flow DSL (named) | (image=x) -> m -> y |
Same |
| Optional parameters | Omit from inputs, default applies |
Same |
| Multiple outputs | "outputs": ["a", "b"] |
Same |
| Reusable in flow | Yes (shared weights) | Yes (stateless) |
Omit outputs (in-place) |
No — outputs always required |
Yes — single-input writes back |
| Multiple applications | No | Yes — inputs as list of dicts |
| Container recursion | No | Yes (annotation-driven) |
| Field overwrite protection | Yes — cannot overwrite existing fields | No — can overwrite (in-place) |
Both use the same config format and flow DSL syntax. The key differences are that DataTransform supports in-place overwrites, multiple applications, and annotation-driven container recursion — features that don't apply to neural network forward passes.
EntryTransform¶
EntryTransform uses constructor-based field keys — field names are passed as _key parameters in __init__(). No set_io() needed.
Constructor Field Keys¶
Field name parameters use the _key suffix to distinguish them from data:
from srforge.transform import EntryTransform
class SelectBands(EntryTransform):
def __init__(self, bands, *, multispectral_key: str, selected_key: str):
self.bands = bands
self.multispectral_key = multispectral_key
self.selected_key = selected_key
super().__init__()
def transform_unbatched(self, entry):
ms = entry[self.multispectral_key]
entry[self.selected_key] = ms[self.bands]
return entry
Use it:
t = SelectBands([0, 1, 2], multispectral_key="raw_ms", selected_key="rgb_only")
entry = t(entry)
# reads entry["raw_ms"], writes entry["rgb_only"]
Optional Field Keys¶
Use Optional[str] = None for optional fields. Check before accessing:
class MaskTransform(EntryTransform):
def __init__(self, *, field_key: str, mask_key: str = None):
self.field_key = field_key
self.mask_key = mask_key
super().__init__()
def transform_unbatched(self, entry):
value = entry[self.field_key]
if self.mask_key and self.mask_key in entry:
value = value * entry[self.mask_key]
entry[self.field_key] = value
return entry
YAML Config¶
Field keys go in params: — no io: key:
select:
_target: SelectBands
params:
bands: [0, 1, 2]
multispectral_key: raw_ms
selected_key: rgb_only
No io: key for EntryTransforms
Using io: with an EntryTransform raises TypeError. Move field keys into params:.
SequentialModel Flow DSL¶
EntryTransforms are opaque steps — the flow DSL just marks when to run the transform. Field mapping is handled by the constructor:
seq = SequentialModel(
modules={"select": SelectBands([0, 1, 2], multispectral_key="raw_ms", selected_key="rgb")},
flow=[" -> select -> "],
)
For different field mappings, use separate instances:
seq = SequentialModel(
modules={
"copy1": CopyFields(field_key="y", output_key="z"),
"copy2": CopyFields(field_key="z", output_key="w"),
},
flow=[" -> copy1 -> ", " -> copy2 -> "],
)
Loss¶
Losses use set_io() with inputs only — they read from Entry fields and return MetricScores, never writing back. See the Losses guide for full details.
from srforge.loss.metrics import L1
loss = L1(weight=1.0)
loss.set_io({"inputs": {"x": "sr", "y": "hr"}})
scores = loss(entry) # reads entry["sr"] → x, entry["hr"] → y
Identity default — when set_io() is not called, parameter names map to same-named Entry fields:
No outputs — passing outputs raises TypeError. Losses don't write to Entry.
No multiple applications — inputs must be a single dict, not a list.
YAML¶
SequentialModel: All Module Types¶
Inside a SequentialModel, Models and DataTransforms get IO from the flow DSL. EntryTransforms get their field keys from the constructor:
model:
_target: srforge.models.SequentialModel
params:
modules:
m1:
_target: MyModel
params: {}
t1:
_target: srforge.transform.entry.CopyFields
params:
field_key: "y"
output_key: "z"
mul:
_target: srforge.transform.data.Multiply
params:
value: 2.0
flow:
- "x -> m1 -> y" # Model: positional mapping
- " -> t1 -> " # EntryTransform: opaque
- "z -> mul -> out" # DataTransform: positional mapping
Key differences between module types in flow
- Model: Supports named input mapping (
(image=x) -> m1). Outputs are always positional. - EntryTransform: Opaque — field keys set via constructor
_keyparams. - DataTransform: Positional or named inputs. Outputs are always positional.
Standalone Use (No Entry)¶
Model and DataTransform can be called directly with raw values, bypassing IO binding:
# Model — no set_io() needed
output = model(some_tensor) # calls forward() directly
# DataTransform (batched)
downsampled = Downsample()(some_tensor) # calls transform() directly
# DataTransform (per-sample)
result = MyPerSampleTransform()(some_tensor) # calls transform() which dispatches
EntryTransform always requires an Entry — it operates on Entry structure.
Validation and Errors¶
Missing Required Constructor Key¶
# EntryTransform missing required field key
SelectBands(bands=[0, 1, 2], multispectral_key="x")
# TypeError: missing required keyword argument 'selected_key'
Unbound DataTransform¶
transform = Multiply(2.0)
transform(entry) # No set_io called
# ValueError: Multiply requires io mappings before it can run.
Dict Outputs Not Supported (for plain Model)¶
# Dict outputs are not allowed for plain Model — use a string or list instead
model.set_io({
"inputs": {"image": "x"},
"outputs": {"output": "y"}, # TypeError!
})
# Correct:
model.set_io({"inputs": {"image": "x"}, "outputs": "y"})
model.set_io({"inputs": {"image": "x"}, "outputs": ["y", "z"]})
GANModel is the exception
GANModel.set_io requires outputs to be a dict mapping its
four fixed port names (real_score_d, fake_score_d, real_score_g,
fake_score_g) to Entry field names — or omitted entirely (identity
default). Passing a string or list raises TypeError. See
GAN Model for details.
Wrong Format¶
# OLD (removed): t.set_io("x")
# OLD (removed): t.set_io(["x", "y"])
# OLD (removed): t.set_io({"x": "y"})
# These all raise TypeError — use the structured format instead.
t.set_io({"inputs": {"image": "x"}}) # Correct
t.set_io({"inputs": {"image": "x"}, "outputs": "y"}) # Correct
Summary¶
| Scenario | What to Do |
|---|---|
| Writing a test or script | component.set_io({"inputs": {...}, "outputs": ...}) |
| Defining an experiment config | Add io: with inputs/outputs keys in YAML |
| Building a SequentialModel | Let the flow DSL handle mapping |
| Prototyping in a notebook | Call the component directly with raw values |
Next: Models — Neural network components that use IO binding