# Inference Plugin Documentation

The Inference Plugin is a core component of the VIPR framework that provides a flexible, step-based workflow system for machine learning inference pipelines.

## Overview

The Inference Plugin implements a generic, extensible workflow for ML/AI inference with the following characteristics:

- **Domain-agnostic design**: Designed to support diverse ML models and data types
- **Step-based architecture**: Five configurable steps that can be customized
- **Hook and filter system**: Extensibility points at each step
- **Type-safe**: Full type checking with beartype
- **Configuration-driven**: YAML-based configuration for all components

The plugin integrates with the VIPR Dynamic Hooks and Filters Extension, allowing hooks and filters to be configured via YAML files rather than requiring code changes. This provides maximum flexibility for adapting workflows to different use cases.

## Data Flow

The inference workflow processes data through a standardized 5-step pipeline. Each step transforms data in a standardized way, ensuring type safety and extensibility.

### Step Input/Output Contracts

```
LoadDataStep:
  Input:  config parameters (dict)
  Output: DataSet(x, y, dx, dy, metadata)

LoadModelStep:  
  Input:  config parameters (dict)
  Output: model instance (Any)

PreprocessStep:
  Input:  DataSet (model available via self.app.inference.model)
  Output: DataSet (preprocessed with transformed errors via filters)
  Note:   Preprocessing is implemented via INFERENCE_PREPROCESS_PRE_FILTER.
          Filters can transform both data and errors (e.g., interpolation, normalization).

PredictionStep:
  Input:  DataSet (model available via self.app.inference.model)
  Output: prediction results (dict)

PostprocessStep:
  Input:  prediction results (dict)
  Output: final results (Any)
```

## DataSet Transfer Object

The {py:class}`vipr.plugins.inference.dataset.DataSet` class implements the Transfer Object Pattern for immutable data transport between pipeline steps.

**Key features:**
- **Immutable**: Arrays are read-only after creation
- **Type-safe**: Full Pydantic validation with beartype
- **Functional updates**: Use `copy_with_updates()` for modifications

**See:** {py:class}`vipr.plugins.inference.dataset.DataSet` for complete API reference and usage examples.

### Extension Points Per Step

Each step provides 6 extension points for customization:

```
AbstractInferenceStep.run():
├── PRE_PRE_FILTER_HOOK      ◄── Hook 1: Before any processing
├── PRE_FILTER               ◄── Filter 1: Transform input
├── POST_PRE_FILTER_HOOK     ◄── Hook 2: After input filter
├── execute()                ◄── Core step logic
├── PRE_POST_FILTER_HOOK     ◄── Hook 3: Before output filter
├── POST_FILTER              ◄── Filter 2: Transform output
└── POST_POST_FILTER_HOOK    ◄── Hook 4: After all processing
```

## Architecture

### Core Components

1. **{py:class}`vipr.plugins.inference.base_inference.BaseInference`**  
   Abstract base providing shared functionality for inference implementations

2. **{py:class}`vipr.plugins.inference.inference.Inference`**  
   Main orchestrator implementing the 5-step workflow with complete hook/filter system

3. **{py:class}`vipr.plugins.inference.abstract_step.AbstractInferenceStep`**  
   Base class for workflow steps with hooks/filters

4. **Workflow Steps:**
   - {py:class}`vipr.plugins.inference.steps.load_data_step.LoadDataInferenceStep` - Loads input data
   - {py:class}`vipr.plugins.inference.steps.load_model_step.LoadModelInferenceStep` - Loads ML models
   - {py:class}`vipr.plugins.inference.steps.preprocess_step.PreprocessInferenceStep` - Preprocesses data
   - {py:class}`vipr.plugins.inference.steps.prediction_step.PredictionInferenceStep` - Executes prediction
   - {py:class}`vipr.plugins.inference.steps.postprocess_step.PostprocessInferenceStep` - Post-processes results




## Extension Points

The inference workflow uses two extension mechanisms:

### Handler-Based Steps

Steps that use pluggable handler implementations:

| Step | Handler Type | Interface | Purpose |
|------|-------------|-----------|---------|
| LoadDataStep | `data_loader` | {py:class}`vipr.interfaces.data_loader.DataLoaderInterface` | Load data from various sources |
| LoadModelStep | `model_loader` | {py:class}`vipr.interfaces.model_loader.ModelLoaderInterface` | Load ML models |
| PredictionStep | `predictor` | {py:class}`vipr.interfaces.predictor.PredictorInterface` | Execute predictions |
| PostprocessStep | `postprocessor` | {py:class}`vipr.interfaces.postprocessor.PostprocessorInterface` | Process results |

### Filter-Based Steps

Steps that primarily use filter transformations:

| Step | Primary Filter | Purpose |
|------|---------------|---------|
| PreprocessStep | `INFERENCE_PREPROCESS_PRE_FILTER` | Preprocess data (e.g., interpolation, normalization, error-bar transformations) |

**Note:** In the current workflow, normalization is typically implemented as part of the preprocess filter chain (often via filter weights).

### Handler Implementation

**Base classes:** {py:class}`vipr.handlers.data_loader.DataLoaderHandler`, {py:class}`vipr.handlers.model_loader.ModelLoaderHandler`, {py:class}`vipr.handlers.predictor.PredictorHandler`

**Pattern:**
1. Inherit from appropriate base handler
2. Implement interface method (`_load_data`, `_load_model`, `_predict`)
3. Return correct type ({py:class}`vipr.plugins.inference.dataset.DataSet` for data loaders)

**Complete examples:** See [Reflectometry Handler Patterns](../external/reflectometry/handlers.md)

## Hook and Filter System

The Inference Plugin provides a comprehensive system for extending functionality through hooks and filters. These can be registered either programmatically in your plugin code or through configuration using the Dynamic Hooks and Filters Extension.

### Available Hooks

**Global Workflow Hooks:**  
See {py:class}`vipr.plugins.inference.base_inference.BaseInference` for hook constants:
- `INFERENCE_BEFORE_START_HOOK`: Triggered before workflow start (setup/reset)
- `INFERENCE_START_HOOK`: Triggered at workflow start
- `INFERENCE_COMPLETE_HOOK`: Triggered at workflow completion

**Step-Level Hooks:**  
See {py:class}`vipr.plugins.inference.abstract_step.AbstractInferenceStep` for all step hook constants.

Where `<STEP>` is: `LOAD_DATA`, `LOAD_MODEL`, `PREPROCESS`, `PREDICTION`, or `POSTPROCESS`

- `INFERENCE_<STEP>_PRE_PRE_FILTER_HOOK` - Before PRE_FILTER execution
- `INFERENCE_<STEP>_POST_PRE_FILTER_HOOK` - After PRE_FILTER, before execute()
- `INFERENCE_<STEP>_PRE_POST_FILTER_HOOK` - After execute(), before POST_FILTER
- `INFERENCE_<STEP>_POST_POST_FILTER_HOOK` - After POST_FILTER execution

**Execution Order:**
```
1. PRE_PRE_FILTER_HOOK     ← Hook before input transformation
2. PRE_FILTER              ← Filter transforms input
3. POST_PRE_FILTER_HOOK    ← Hook after input transformation
4. execute()               ← Core step logic
5. PRE_POST_FILTER_HOOK    ← Hook before output transformation
6. POST_FILTER             ← Filter transforms output
7. POST_POST_FILTER_HOOK   ← Hook after output transformation
```

### Available Filters

**Step-Level Filters** (for each step):
- `INFERENCE_<STEP>_PRE_FILTER`: Transform input data before execute()
- `INFERENCE_<STEP>_POST_FILTER`: Transform output data after execute()

### Registering Hooks and Filters

You can register hooks and filters in two ways:

#### 1. Programmatically (in your plugin)

```python
# In your plugin
def _post_setup(self, app):
    # Register a hook
    app.hook.register('INFERENCE_START_HOOK', self.on_inference_start)
    
    # Register a filter
    app.filter.register('INFERENCE_PREPROCESS_PRE_FILTER', self.custom_preprocess)

def on_inference_start(self, app):
    app.log.info("Custom inference starting...")

def custom_preprocess(self, data: DataSet, **kwargs) -> DataSet:
    # Custom preprocessing logic (e.g., normalization)
    return data.copy_with_updates(
        x=normalized_x,
        y=normalized_y,
        metadata={**data.metadata, 'preprocessed': True}
    )
```

#### 2. Via Configuration (using Dynamic Hooks and Filters Extension)

The recommended approach for maximum flexibility is to use the Dynamic Hooks and Filters Extension, which allows you to configure hooks and filters in your YAML file.

> **TODO**: Dynamic Hooks and Filters Extension documentation will be migrated soon.

## Configuration

### Basic Configuration Structure

```yaml
vipr:
  inference:
    # Step configurations
    load_data:
      handler: your_data_loader
      parameters:
        file_path: data/input.csv
        delimiter: ','

    load_model:
      handler: your_model_loader
      parameters:
        model_path: models/my_model.pt
        device: cuda

    preprocess:
      # Usually filter-driven (INFERENCE_PREPROCESS_PRE_FILTER)
      handler: ''
      parameters: {}

    prediction:
      handler: your_predictor
      parameters:
        batch_size: 32

    postprocess:
      handler: your_postprocessor
      parameters:
        format: json

    # Dynamic hooks and filters can be configured here
    # See Dynamic Hooks and Filters Extension documentation for details
    hooks: {}
    filters: {}
```

### Dynamic Hook/Filter Configuration

The Inference Plugin integrates with the `dynamic_hooks_filters` extension for configuration-based hook and filter registration. This allows you to define hooks and filters in your YAML configuration instead of hardcoding them in plugin code.

> **Note**: For detailed information about configuring dynamic hooks and filters, please refer to the [Dynamic Hooks and Filters Extension Documentation](../extensions/dynamic_hooks_filters.md).

## Usage Example

### Creating a Controller

```python
from cement import Controller, ex

class MLController(Controller):
    class Meta:
        label = 'ml'
        stacked_on = 'base'
        stacked_type = 'nested'
    
    @ex(help='Run ML inference')
    def predict(self):
        # The inference workflow is already available as self.app.inference
        # (registered by the inference plugin during app setup)
        
        # Run inference workflow
        result = self.app.inference.run()
        
        # Access intermediate results if needed
        self.app.log.info(f"Model used: {self.app.inference.model}")
        self.app.log.info(f"Preprocessed data shape: {self.app.inference.preprocessed_data.x.shape}")
        
        # Return final result
        self.app.log.info(f"Prediction complete")
        return result
```

### Running via CLI

```bash
# With configuration file
vipr --config config/ml_config.yaml ml predict
```
