# Controllers Introduction
Controllers are the **behavior adapters** between abstract pipeline steps and concrete execution. They're the core extension seam of the NIRS4ALL engine, making it flexible and framework-agnostic.
## Controller Philosophy
A step definition alone is not enough — execution requires **context-aware behavior**.
Consider this pipeline step:
```python
PLSRegression(n_components=10)
```
To execute this, you need to know:
- Are we in train mode or predict mode?
- Which fold are we on?
- Which partition (train/val/test)?
- Which processing chains are active?
- Which branch context are we in?
- Where should artifacts be stored?
Controllers provide this **context-aware behavior**, interpreting operator intent based on current execution state.
## Why Not Just Execute Directly?
Many libraries directly execute step objects (`step.fit(X, y)`). NIRS4ALL routes steps through controllers because:
1. **Framework diversity** — sklearn, TensorFlow, PyTorch, JAX all have different training interfaces
2. **Keyword semantics** — `model`, `y_processing`, `branch`, `merge` need specialized handling
3. **Mode awareness** — train vs predict vs explain have different behaviors
4. **Branching orchestration** — parallel subflows need coordination
5. **Artifact management** — saving and loading fitted objects requires coordination
::::{admonition} Key Insight
:class: tip
Controllers turn a **declarative pipeline** (what you want) into an **imperative execution** (how to do it), while keeping your pipeline definitions simple and framework-agnostic.
::::
## Controller Registration Model
Controllers are registered in a **global registry** using the `@register_controller` decorator:
```python
from nirs4all.controllers import register_controller, OperatorController
@register_controller
class MyCustomController(OperatorController):
priority = 50 # Lower = higher priority
@classmethod
def matches(cls, step, operator, keyword) -> bool:
return isinstance(operator, MyOperatorType)
def execute(self, step_info, dataset, context, runtime_context, **kwargs):
# Execute the step
pass
```
**Matching and routing are dynamic**: a controller is selected when its `matches()` classmethod returns `True`, with `priority` deciding conflicts.
This avoids hardcoded if-else execution trees and makes extension easy.
## Controller Base Contract
All controllers inherit from `OperatorController` (abstract base class). The required interface:
| Method | Type | Purpose |
| --- | --- | --- |
| `matches(step, operator, keyword)` | classmethod | Determine if this controller handles the step |
| `use_multi_source()` | classmethod | Whether to iterate over sources |
| `supports_prediction_mode()` | classmethod | Whether to run during prediction |
| `execute(step_info, dataset, context, runtime_context)` | instance | Perform the step's logic |
Each controller also declares a numeric `priority` (lower = higher precedence).
::::{admonment} Execution Contract
:class: note
The `execute()` method:
- **Receives**: `step_info`, `dataset`, `context`, `runtime_context`
- **Returns**: `(context, StepOutput)` tuple
- **Can mutate**: `dataset`, `context` (but must return updated context)
- **Can emit**: artifacts via `runtime_context.artifact_registry`
::::
## Controller Families
NIRS4ALL includes several controller families:
### Transform Controllers
Handle feature and target transformations:
- **`TransformerMixinController`** (priority 10) — general feature-space transforms (SNV, MSC, StandardScaler, etc.)
- **`YTransformerMixinController`** (priority 5) — target-side transforms (`y_processing`)
### Model Controllers
All inherit from `BaseModelController`:
- **`SklearnModelController`** (priority 6) — sklearn-like estimators (PLSRegression, RandomForest, etc.)
- **`TensorFlowModelController`** (priority 4) — TensorFlow models
- **`PyTorchModelController`** (priority 4) — PyTorch models
- **`JaxModelController`** (priority 4) — JAX models
- **`AutoGluonModelController`** (priority 5) — AutoGluon configs
- **`MetaModelController`** (priority 5) — stacking/meta-model behavior
### Data-Flow Controllers
Handle complex data transformations and routing:
- **`BranchController`** (priority 5) — branch orchestration (duplication and separation branches)
- **`MergeController`** (priority 5) — branch output reconciliation
- **`FeatureAugmentationController`** (priority 10) — repeated preprocessing orchestration
- **`SampleAugmentationController`** (priority 10) — synthetic sample generation
- **`TagController`** (priority 5) — sample annotation
- **`ExcludeController`** (priority 5) — sample exclusion
- **`FeatureSelectionController`** (priority 5) — feature selection
- **`ResamplerController`** (priority 5) — spectral resampling
### Splitter Controllers
Handle cross-validation fold generation:
- **`CrossValidatorController`** (priority 10) — fold generation from CV splitters
- **`FoldFileLoaderController`** (priority 9) — load folds from files
### Chart Controllers
Generate diagnostic charts:
- **`SpectraChartController`** — spectra visualization
- **`YChartController`** — target distribution
- **`FoldChartController`** — fold composition
- **`ExclusionChartController`** — exclusion diagnostics
- **`AugmentationChartController`** — augmentation diagnostics
- **`SpectralDistributionController`** — spectral distribution analysis
All chart controllers have priority 10.
### Fallback Controller
- **`DummyController`** (priority 1000) — catch-all for unhandled operators (logs a warning)
## Controller Family Map
```mermaid
flowchart TD
A[Controller Registry] --> B[Transform Controllers]
A --> C[Model Controllers]
A --> D[Data Controllers]
A --> E[Splitter Controllers]
A --> F[Chart Controllers]
A --> G[Fallback Controller]
D --> D1[Branch]
D --> D2[Merge]
D --> D3[Feature Augmentation]
D --> D4[Sample Augmentation]
D --> D5[Tag and Exclude]
D --> D6[Repetition Handlers]
C --> C1[Sklearn]
C --> C2[TensorFlow]
C --> C3[PyTorch]
C --> C4[JAX]
C --> C5[AutoGluon]
C --> C6[MetaModel]
```
## Controller Interaction Patterns
Controllers interact with three main surfaces:
### Dataset Interaction
- **Read/mutate feature processings** — add new processing chains
- **Read/mutate folds** — set fold assignments
- **Read/mutate exclusions** — mark samples as excluded
- **Read/mutate target processings** — transform targets
- **Read/mutate tags** — annotate samples
### ExecutionContext Interaction
- **Update selector state** — change partition, processing, layout, etc.
- **Update pipeline state** — advance step number, change execution mode
- **Update metadata flags** — track controller coordination data
- **Update custom state** — store controller-specific data (e.g., branch paths)
### Artifacts Interaction
- **Emit artifacts in train mode** — save fitted models/transformers
- **Consume artifacts in predict mode** — load fitted artifacts
## Controller Priority Strategy
Lower priority values win earlier. Use low priority for **specific behaviors** and higher priority for **generic ones**.
This keeps specific handlers from being shadowed by generic ones.
::::{admonition} Priority Examples
:class: note
**Specific wins over generic:**
- `TensorFlowModelController` (priority 4) beats `SklearnModelController` (priority 6) for TensorFlow objects
- `YTransformerMixinController` (priority 5) beats `TransformerMixinController` (priority 10) for `y_processing` keyword
**Generic catches all:**
- `DummyController` (priority 1000) catches anything unmatched
::::
## Controller Extension Pattern
To add custom behavior, follow this pattern:
### Step 1: Define Controller Class
```python
from nirs4all.controllers import register_controller, OperatorController
@register_controller
class MyCustomController(OperatorController):
priority = 50 # Choose appropriate priority
@classmethod
def matches(cls, step, operator, keyword) -> bool:
# Return True if this controller handles the step
return isinstance(operator, MyOperatorClass)
@classmethod
def use_multi_source(cls) -> bool:
# Return True if this operator should iterate over sources
return False
@classmethod
def supports_prediction_mode(cls) -> bool:
# Return True if this operator runs during prediction
return True
```
### Step 2: Implement Execute Logic
```python
def execute(self, step_info, dataset, context, runtime_context, **kwargs):
# Get operator from step_info
operator = step_info.operator
# Get current partition and processing chains
selector = context.selector
partition = selector.partition
# Get features and targets
X, y = dataset.get_X_y(selector)
# Execute operator logic
if context.state.execution_mode == "train":
# Fit operator
operator.fit(X, y)
# Save artifact
artifact_id = runtime_context.artifact_registry.register(
operator,
step_index=step_info.step_index,
artifact_type="transform",
)
else:
# Load artifact
operator = runtime_context.artifact_loader.load_artifact(
step_index=step_info.step_index,
)
# Transform data
X_transformed = operator.transform(X)
# Update dataset with new processing
dataset.add_processing(X_transformed, processing_name=f"step_{step_info.step_index}")
# Update context selector to use new processing
new_selector = selector.with_processing(processing=f"step_{step_info.step_index}")
context = context.with_selector(new_selector)
# Return updated context and step output
return context, StepOutput(success=True)
```
### Step 3: Register and Import
The `@register_controller` decorator automatically registers the controller. Just ensure the module is imported at runtime:
```python
# In your __init__.py or main script
from mypackage.controllers import MyCustomController
```
## Controller Extension Examples
### Example 1: Custom Transform Controller
```python
from nirs4all.controllers import register_controller, OperatorController
from nirs4all.operators.transforms import MyCustomTransform
@register_controller
class MyCustomTransformController(OperatorController):
priority = 15 # Higher than TransformerMixinController (10)
@classmethod
def matches(cls, step, operator, keyword) -> bool:
return isinstance(operator, MyCustomTransform)
@classmethod
def supports_prediction_mode(cls) -> bool:
return True
def execute(self, step_info, dataset, context, runtime_context, **kwargs):
# Custom execution logic
pass
```
### Example 2: Custom Keyword Controller
```python
@register_controller
class MyKeywordController(OperatorController):
priority = 10
@classmethod
def matches(cls, step, operator, keyword) -> bool:
return keyword == "my_custom_keyword"
def execute(self, step_info, dataset, context, runtime_context, **kwargs):
# Custom keyword behavior
pass
```
Now you can use:
```python
pipeline = [
{"my_custom_keyword": SomeOperator()},
]
```
## Extension Quality Guidelines
When extending with custom controllers:
1. **Define explicit failure behavior** — don't silently fall back to generic behavior
2. **Avoid silent fallback logic** — log warnings when behavior is ambiguous
3. **Log enough metadata for debugging** — include step_index, operator type, keyword
4. **Test train and predict mode separately** — ensure symmetry
5. **Validate branch and multi-source compatibility** — if relevant to your operator
## Advanced Topics
Multi-Source Controllers
If your operator needs to iterate over sources:
```python
@classmethod
def use_multi_source(cls) -> bool:
return True
```
The execution engine will call `execute()` once per source, updating `context.selector.source_index` each time.
Branch-Aware Controllers
Access branch context via:
```python
branch_id = context.custom.get("branch_id")
branch_name = context.custom.get("branch_name")
```
Emit branch-specific artifacts:
```python
artifact_id = runtime_context.artifact_registry.register(
operator,
branch_id=branch_id,
)
```
Fold-Aware Controllers
Model controllers automatically get fold context:
```python
fold_id = context.selector.fold_id
```
Emit fold-specific artifacts:
```python
artifact_id = runtime_context.artifact_registry.register(
model,
fold_id=fold_id,
)
```
## Next Steps
Now that you understand controllers, explore:
- **[Pipeline Workflow](../concepts/pipeline_workflow.md)** — See how controllers fit into execution
- **[Data Workflow](../concepts/data_workflow.md)** — Learn how controllers interact with datasets
Or dive into the [Developer Guide](index.md) for controller implementation details.
```{seealso}
**Related Examples:**
- [D02: Custom Controllers](../../examples/developer/06_internals/D02_custom_controllers.py) - Complete guide to creating custom controllers
- [U01: Multi-Model](../../examples/user/04_models/U01_multi_model.py) - See how model controller handles multiple models
- [D01: Branching Basics](../../examples/developer/01_advanced_pipelines/D01_branching_basics.py) - Branch controller in action
```