diff --git a/.github/workflows/lint.yml b/.github/workflows/lint.yml
new file mode 100644
index 0000000..77a4d77
--- /dev/null
+++ b/.github/workflows/lint.yml
@@ -0,0 +1,81 @@
+name: Lint
+
+on:
+ push:
+ branches: [main]
+ pull_request:
+ branches: [main]
+
+jobs:
+ ruff:
+ name: Ruff (Linter & Formatter)
+ runs-on: ubuntu-latest
+ steps:
+ - name: Checkout code
+ uses: actions/checkout@v4
+
+ - name: Set up Python
+ uses: actions/setup-python@v5
+ with:
+ python-version: "3.11"
+
+ - name: Install Ruff
+ run: pip install ruff
+
+ - name: Run Ruff linter
+ run: ruff check .
+
+ - name: Run Ruff formatter check
+ run: ruff format --check .
+
+ type-check:
+ name: Type Checking (mypy)
+ runs-on: ubuntu-latest
+ steps:
+ - name: Checkout code
+ uses: actions/checkout@v4
+
+ - name: Set up Python
+ uses: actions/setup-python@v5
+ with:
+ python-version: "3.11"
+
+ - name: Install uv
+ uses: astral-sh/setup-uv@v3
+ with:
+ version: "latest"
+
+ - name: Install dependencies
+ run: |
+ uv pip install --system -e .
+ uv pip install --system mypy types-requests
+
+ - name: Run mypy
+ run: mypy faim_sdk/ --ignore-missing-imports --no-strict-optional
+ continue-on-error: true
+
+ package-check:
+ name: Package Build Check
+ runs-on: ubuntu-latest
+ steps:
+ - name: Checkout code
+ uses: actions/checkout@v4
+
+ - name: Set up Python
+ uses: actions/setup-python@v5
+ with:
+ python-version: "3.11"
+
+ - name: Install build dependencies
+ run: pip install poetry build twine
+
+ - name: Build package
+ run: poetry build
+
+ - name: Check package with twine
+ run: twine check dist/*
+
+ - name: Verify package contents
+ run: |
+ tar -tzf dist/*.tar.gz | grep -E "faim_sdk|faim_client"
+ unzip -l dist/*.whl | grep -E "faim_sdk|faim_client"
diff --git a/.github/workflows/test.yml b/.github/workflows/test.yml
new file mode 100644
index 0000000..a263b44
--- /dev/null
+++ b/.github/workflows/test.yml
@@ -0,0 +1,81 @@
+name: Test
+
+on:
+ push:
+ branches: [main]
+ pull_request:
+ branches: [main]
+
+jobs:
+ test:
+ name: Test Python ${{ matrix.python-version }}
+ runs-on: ubuntu-latest
+ if: false # Tests temporarily disabled
+ strategy:
+ fail-fast: false
+ matrix:
+ python-version: ["3.11", "3.12"]
+
+ steps:
+ - name: Checkout code
+ uses: actions/checkout@v4
+
+ - name: Set up Python ${{ matrix.python-version }}
+ uses: actions/setup-python@v5
+ with:
+ python-version: ${{ matrix.python-version }}
+
+ - name: Install uv
+ uses: astral-sh/setup-uv@v3
+ with:
+ version: "latest"
+
+ - name: Install dependencies
+ run: |
+ uv pip install --system -e .
+ uv pip install --system pytest pytest-cov pytest-asyncio
+
+ - name: Run unit tests
+ run: |
+ pytest tests/unit/ -v --cov=faim_sdk --cov=faim_client --cov-report=xml --cov-report=term
+
+ - name: Upload coverage to Codecov
+ uses: codecov/codecov-action@v4
+ if: matrix.python-version == '3.11'
+ with:
+ file: ./coverage.xml
+ fail_ci_if_error: false
+ token: ${{ secrets.CODECOV_TOKEN }}
+
+ test-examples:
+ name: Test Examples
+ runs-on: ubuntu-latest
+ if: false # Tests temporarily disabled
+ steps:
+ - name: Checkout code
+ uses: actions/checkout@v4
+
+ - name: Set up Python 3.11
+ uses: actions/setup-python@v5
+ with:
+ python-version: "3.11"
+
+ - name: Install uv
+ uses: astral-sh/setup-uv@v3
+ with:
+ version: "latest"
+
+ - name: Install dependencies with viz extras
+ run: |
+ uv pip install --system -e ".[viz]"
+ uv pip install --system jupyterlab pandas statsmodels
+
+ - name: Check example notebooks can be read
+ run: |
+ python -c "import nbformat; nbformat.read('examples/flowstate_simple_example.ipynb', as_version=4)"
+ python -c "import nbformat; nbformat.read('examples/model_comparison_example.ipynb', as_version=4)"
+ python -c "import nbformat; nbformat.read('examples/model_comparison_simple.ipynb', as_version=4)"
+
+ - name: Verify eval package imports
+ run: |
+ python -c "from faim_sdk.eval import mse, mase, crps_from_quantiles, plot_forecast; print('Eval imports successful')"
diff --git a/.gitignore b/.gitignore
index 40b808b..349373e 100644
--- a/.gitignore
+++ b/.gitignore
@@ -24,4 +24,5 @@ dmypy.json
openapi.json
pyproject.toml
-CLAUDE.md
\ No newline at end of file
+CLAUDE.md
+/poetry.lock
diff --git a/README.md b/README.md
index 6637fc0..5cee745 100644
--- a/README.md
+++ b/README.md
@@ -1,6 +1,21 @@
# FAIM SDK
-Python SDK for FAIM time-series forecasting with foundation AI models.
+[](https://badge.fury.io/py/faim-sdk)
+[](https://www.python.org/downloads/)
+[](https://opensource.org/licenses/Apache-2.0)
+
+Production-ready Python SDK for FAIM (Foundation AI Models) - a high-performance time-series forecasting platform powered by foundation models.
+
+## Features
+
+- **๐ Multiple Foundation Models**: FlowState, Amazon Chronos 2.0, TiRex
+- **๐ Type-Safe API**: Full type hints with Pydantic validation
+- **โก High Performance**: Optimized Apache Arrow serialization with zero-copy operations
+- **๐ฏ Probabilistic & Deterministic**: Point forecasts, quantiles, and samples
+- **๐ Async Support**: Built-in async/await support for concurrent requests
+- **๐ Rich Error Handling**: Machine-readable error codes with detailed diagnostics
+- **๐งช Battle-Tested**: Production-ready with comprehensive error handling
+- **๐ Evaluation Tools**: Built-in metrics (MSE, MASE, CRPS) and visualization utilities
## Installation
@@ -8,89 +23,160 @@ Python SDK for FAIM time-series forecasting with foundation AI models.
pip install faim-sdk
```
+## Authentication
+
+Get your free API key at **[https://faim.it.com/](https://faim.it.com/)**
+
+```python
+from faim_sdk import ForecastClient
+
+# Initialize client with your API key
+client = ForecastClient(api_key="your-api-key")
+```
+
## Quick Start
```python
import numpy as np
-from faim_sdk import ForecastClient, FlowStateForecastRequest
-from faim_client.models import ModelName
+from faim_sdk import ForecastClient, Chronos2ForecastRequest
# Initialize client
-client = ForecastClient(
- base_url="http://localhost:8003",
- timeout=60.0
-)
+client = ForecastClient(api_key="your-api-key")
# Prepare your time-series data
# Shape: (batch_size, sequence_length, features)
-data = np.random.randn(1, 100, 1).astype(np.float32)
+data = np.random.randn(32, 100, 1).astype(np.float32)
-# Create forecast request
-request = FlowStateForecastRequest(
+# Create probabilistic forecast request
+request = Chronos2ForecastRequest(
x=data,
- horizon=10,
- model_version="1"
+ horizon=24, # Forecast 24 steps ahead
+ output_type="quantiles",
+ quantiles=[0.1, 0.5, 0.9] # 10th, 50th (median), 90th percentiles
)
-# Generate forecast
-response = client.forecast(ModelName.FLOWSTATE, request)
+# Generate forecast - model inferred automatically from request type
+response = client.forecast(request)
# Access predictions
-print(response.point) # Shape: (batch_size, horizon, features)
-print(response.metadata) # Model metadata
+print(response.quantiles.shape) # (32, 24, 3, 1)
+print(response.metadata) # Model version, inference time, etc.
```
-## Models
+## Input/Output Format
-### FlowState
+### Input Data Format
+
+**All models require 3D input arrays:**
+
+```python
+# Shape: (batch_size, sequence_length, features)
+x = np.array([
+ [[1.0], [2.0], [3.0]], # Series 1
+ [[4.0], [5.0], [6.0]] # Series 2
+]) # Shape: (2, 3, 1)
+```
+
+- **batch_size**: Number of independent time series
+- **sequence_length**: Historical data points (context window)
+- **features**: Number of variables per time step (use 1 for univariate)
+
+**Important**: 2D input will raise a validation error. Always provide 3D arrays.
+
+### Output Data Format
+
+**Point Forecasts** (3D):
+```python
+response.point # Shape: (batch_size, horizon, features)
+```
+
+**Quantile Forecasts** (4D):
+```python
+response.quantiles # Shape: (batch_size, horizon, num_quantiles, features)
+# Example: (32, 24, 5, 1) = 32 series, 24 steps ahead, 5 quantiles, 1 feature
+```
+
+### Univariate vs Multivariate
+
+- **Chronos2**: โ
Supports multivariate forecasting (multiple features)
+- **FlowState**: โ ๏ธ Univariate only - automatically transforms multivariate input
+- **TiRex**: โ ๏ธ Univariate only - automatically transforms multivariate input
+
+When you provide multivariate input (features > 1) to FlowState or TiRex, the SDK automatically:
+1. Issues a warning
+2. Forecasts each feature independently
+3. Reshapes the output back to your original structure
+
+```python
+# Multivariate input to FlowState
+data = np.random.randn(2, 100, 3) # 2 series, 3 features
+request = FlowStateForecastRequest(x=data, horizon=24, prediction_type="mean")
+
+# Warning: "FlowState model only supports univariate forecasting..."
+response = client.forecast(request)
-Point forecasting model optimized for deterministic predictions.
+# Output is automatically reshaped
+print(response.point.shape) # (2, 24, 3) - original structure preserved
+```
+
+## Available Models
+
+### FlowState
```python
from faim_sdk import FlowStateForecastRequest
request = FlowStateForecastRequest(
x=data,
- horizon=10,
- model_version="1",
- output_type="point", # Options: "point", "quantiles", "samples"
- scale_factor=1.0, # Optional normalization
- prediction_type="mean" # Options: "mean", "median", "quantile"
+ horizon=24,
+ model_version="latest",
+ output_type="point",
+ scale_factor=1.0, # Optional: normalization factor, for details check: https://huggingface.co/ibm-granite/granite-timeseries-flowstate-r1
+ prediction_type="mean" # Options: "mean", "median"
)
-```
-### ToTo
+response = client.forecast(request)
+print(response.point.shape) # (batch_size, 24, features)
+```
-Probabilistic forecasting model with quantile and sample-based predictions.
+### Chronos 2.0
```python
-from faim_sdk import ToToForecastRequest
+from faim_sdk import Chronos2ForecastRequest
-# Quantile predictions
-request = ToToForecastRequest(
+# Quantile-based probabilistic forecast
+request = Chronos2ForecastRequest(
x=data,
- horizon=10,
- model_version="1",
+ horizon=24,
output_type="quantiles",
- quantiles=[0.1, 0.5, 0.9] # 10th, 50th, 90th percentiles
+ quantiles=[0.05, 0.25, 0.5, 0.75, 0.95] # Full distribution
)
-# Sample-based predictions
-request = ToToForecastRequest(
+response = client.forecast(request)
+print(response.quantiles.shape) # (batch_size, 24, 5)
+```
+
+### TiRex
+
+```python
+from faim_sdk import TiRexForecastRequest
+
+request = TiRexForecastRequest(
x=data,
- horizon=10,
- model_version="1",
- output_type="samples",
- num_samples=100
+ horizon=24,
+ output_type="point"
)
+
+response = client.forecast(request)
+print(response.point.shape) # (batch_size, 24, features)
```
## Response Format
-All forecasts return a `ForecastResponse` object:
+All forecasts return a `ForecastResponse` object with predictions and metadata:
```python
-response = client.forecast(ModelName.TOTO, request)
+response = client.forecast(request)
# Access predictions based on output_type
if response.point is not None:
@@ -98,53 +184,279 @@ if response.point is not None:
if response.quantiles is not None:
quantiles = response.quantiles # Shape: (batch_size, horizon, num_quantiles)
+ # Lower quantiles for uncertainty bounds
+ lower_bound = quantiles[:, :, 0] # 10th percentile
+ median = quantiles[:, :, 1] # 50th percentile (median)
+ upper_bound = quantiles[:, :, 2] # 90th percentile
if response.samples is not None:
samples = response.samples # Shape: (batch_size, horizon, num_samples)
# Access metadata
-print(response.metadata) # {'model_name': 'toto', 'model_version': '1'}
+print(response.metadata)
+# {'model_name': 'chronos2', 'model_version': '1.0', 'inference_time_ms': 123}
```
-## Async Usage
+## Evaluation & Metrics
+
+The SDK includes a comprehensive evaluation toolkit (`faim_sdk.eval`) for measuring forecast quality with standard metrics and visualizations.
+
+### Installation
+
+For visualization support, install with the viz extra:
+
+```bash
+pip install faim-sdk[viz]
+```
+
+### Available Metrics
+
+#### Mean Squared Error (MSE)
+
+Measures average squared difference between predictions and ground truth.
```python
-async with ForecastClient(base_url="http://localhost:8003") as client:
- response = await client.forecast_async(ModelName.FLOWSTATE, request)
- print(response.point)
+from faim_sdk.eval import mse
+
+# Evaluate point forecast
+mse_score = mse(test_data, response.point, reduction='mean')
+print(f"MSE: {mse_score:.4f}")
+
+# Per-sample MSE
+mse_per_sample = mse(test_data, response.point, reduction='none')
+print(f"MSE per sample shape: {mse_per_sample.shape}") # (batch_size,)
```
-## Examples
+#### Mean Absolute Scaled Error (MASE)
+
+Scale-independent metric comparing forecast to naive baseline (better than MAPE for series with zeros).
+
+```python
+from faim_sdk.eval import mase
+
+# MASE requires training data for baseline
+mase_score = mase(test_data, response.point, train_data, reduction='mean')
+print(f"MASE: {mase_score:.4f}")
+
+# Interpretation:
+# MASE < 1: Better than naive baseline
+# MASE = 1: Equivalent to naive baseline
+# MASE > 1: Worse than naive baseline
+```
+
+#### Continuous Ranked Probability Score (CRPS)
+
+Proper scoring rule for probabilistic forecasts - generalizes MAE to distributions.
+
+```python
+from faim_sdk.eval import crps_from_quantiles
+
+# Evaluate probabilistic forecast with quantiles
+crps_score = crps_from_quantiles(
+ test_data,
+ response.quantiles,
+ quantile_levels=[0.1, 0.5, 0.9],
+ reduction='mean'
+)
+print(f"CRPS: {crps_score:.4f}")
+```
+
+### Visualization
+
+Plot forecasts with training context and ground truth:
+
+```python
+from faim_sdk.eval import plot_forecast
+
+# Plot single sample (remember to index batch dimension!)
+fig, ax = plot_forecast(
+ train_data=train_data[0], # (seq_len, features) - 2D array
+ forecast=response.point[0], # (horizon, features) - 2D array
+ test_data=test_data[0], # (horizon, features) - optional
+ title="Time Series Forecast"
+)
+
+# Save to file
+fig.savefig("forecast.png", dpi=300, bbox_inches="tight")
+```
+
+#### Multi-Feature Visualization
-See the `examples/` directory for complete notebook examples:
-- `flowstate_simple_example.ipynb` - Point forecasting with FlowState
-- `toto_simple_example.ipynb` - Probabilistic forecasting with ToTo
+```python
+# Option 1: All features on same plot
+fig, ax = plot_forecast(
+ train_data[0],
+ response.point[0],
+ test_data[0],
+ features_on_same_plot=True,
+ feature_names=["Temperature", "Humidity", "Pressure"]
+)
+
+# Option 2: Separate subplots per feature
+fig, axes = plot_forecast(
+ train_data[0],
+ response.point[0],
+ test_data[0],
+ features_on_same_plot=False,
+ feature_names=["Temperature", "Humidity", "Pressure"]
+)
+```
+
+### Complete Evaluation Example
+
+```python
+import numpy as np
+from faim_sdk import ForecastClient, Chronos2ForecastRequest
+from faim_sdk.eval import mse, mase, crps_from_quantiles, plot_forecast
+
+# Initialize client
+client = ForecastClient()
+
+# Prepare data splits
+train_data = np.random.randn(32, 100, 1)
+test_data = np.random.randn(32, 24, 1)
+
+# Generate forecast
+request = Chronos2ForecastRequest(
+ x=train_data,
+ horizon=24,
+ output_type="quantiles",
+ quantiles=[0.1, 0.5, 0.9]
+)
+response = client.forecast(request)
+
+# Evaluate point forecast (use median)
+point_pred = response.quantiles[:, :, 1:2] # Extract median, keep 3D shape
+mse_score = mse(test_data, point_pred)
+mase_score = mase(test_data, point_pred, train_data)
+
+# Evaluate probabilistic forecast
+crps_score = crps_from_quantiles(
+ test_data,
+ response.quantiles,
+ quantile_levels=[0.1, 0.5, 0.9]
+)
+
+print(f"MSE: {mse_score:.4f}")
+print(f"MASE: {mase_score:.4f}")
+print(f"CRPS: {crps_score:.4f}")
+
+# Visualize best and worst predictions
+mse_per_sample = mse(test_data, point_pred, reduction='none')
+best_idx = np.argmin(mse_per_sample)
+worst_idx = np.argmax(mse_per_sample)
+
+fig1, ax1 = plot_forecast(
+ train_data[best_idx],
+ point_pred[best_idx],
+ test_data[best_idx],
+ title=f"Best Forecast (MSE: {mse_per_sample[best_idx]:.4f})"
+)
+fig1.savefig("best_forecast.png")
+
+fig2, ax2 = plot_forecast(
+ train_data[worst_idx],
+ point_pred[worst_idx],
+ test_data[worst_idx],
+ title=f"Worst Forecast (MSE: {mse_per_sample[worst_idx]:.4f})"
+)
+fig2.savefig("worst_forecast.png")
+```
## Error Handling
-The SDK provides specific exception types for different error scenarios:
+The SDK provides **machine-readable error codes** for robust error handling:
```python
from faim_sdk import (
- ModelNotFoundError,
+ ForecastClient,
+ Chronos2ForecastRequest,
ValidationError,
- TimeoutError,
- NetworkError,
- SerializationError
+ AuthenticationError,
+ RateLimitError,
+ ModelNotFoundError,
+ ErrorCode
)
try:
- response = client.forecast(ModelName.FLOWSTATE, request)
-except ModelNotFoundError:
- print("Model or version not found")
-except ValidationError:
- print("Invalid request parameters")
-except TimeoutError:
- print("Request timed out")
-except NetworkError:
- print("Network communication failed")
-except SerializationError:
- print("Failed to serialize/deserialize data")
+ request = Chronos2ForecastRequest(x=data, horizon=24, quantiles=[0.1, 0.5, 0.9])
+ response = client.forecast(request)
+
+except AuthenticationError as e:
+ # Handle authentication failures (401, 403)
+ print(f"Authentication failed: {e.message}")
+ print(f"Request ID: {e.error_response.request_id}")
+
+except ValidationError as e:
+ # Handle invalid request parameters (422)
+ if e.error_code == ErrorCode.INVALID_SHAPE:
+ print(f"Shape error: {e.error_response.detail}")
+ # Fix shape and retry
+ elif e.error_code == ErrorCode.MISSING_REQUIRED_FIELD:
+ print(f"Missing field: {e.error_response.detail}")
+
+except RateLimitError as e:
+ # Handle rate limiting (429)
+ print("Rate limit exceeded - implementing exponential backoff")
+ retry_after = e.error_response.metadata.get('retry_after', 60)
+ time.sleep(retry_after)
+
+except ModelNotFoundError as e:
+ # Handle model/version not found (404)
+ print(f"Model not found: {e.message}")
+```
+
+### Exception Hierarchy
+
+```
+FAIMError (base)
+โโโ APIError
+โ โโโ AuthenticationError (401, 403)
+โ โโโ InsufficientFundsError (402)
+โ โโโ ModelNotFoundError (404)
+โ โโโ PayloadTooLargeError (413)
+โ โโโ ValidationError (422)
+โ โโโ RateLimitError (429)
+โ โโโ InternalServerError (500)
+โ โโโ ServiceUnavailableError (503, 504)
+โโโ NetworkError
+โโโ SerializationError
+โโโ TimeoutError
+โโโ ConfigurationError
+```
+
+## Async Support
+
+The SDK supports async operations for concurrent requests:
+
+```python
+import asyncio
+from faim_sdk import ForecastClient, Chronos2ForecastRequest
+
+async def forecast_multiple_series():
+ client = ForecastClient(
+ api_key="your-api-key"
+ )
+
+ # Create multiple requests
+ requests = [
+ Chronos2ForecastRequest(x=data1, horizon=24),
+ Chronos2ForecastRequest(x=data2, horizon=24),
+ Chronos2ForecastRequest(x=data3, horizon=24),
+ ]
+
+ # Execute concurrently
+ async with client:
+ tasks = [
+ client.forecast_async(req)
+ for req in requests
+ ]
+ responses = await asyncio.gather(*tasks)
+
+ return responses
+
+# Run async forecasts
+responses = asyncio.run(forecast_multiple_series())
```
## Configuration
@@ -152,63 +464,121 @@ except SerializationError:
### Client Options
```python
-# Without authentication
+from faim_sdk import ForecastClient
+
+# Basic configuration
client = ForecastClient(
- base_url="https://api.example.com",
- timeout=120.0, # Request timeout in seconds
- verify_ssl=True, # SSL certificate verification
- **httpx_kwargs # Additional httpx.Client arguments
+ timeout=120.0, # Request timeout in seconds (default: 120)
+ verify_ssl=True, # SSL certificate verification (default: True)
)
# With API key authentication
client = ForecastClient(
- base_url="https://api.example.com",
- api_key="your-secret-api-key", # API key for authentication
+ api_key="your-secret-api-key",
+ timeout=120.0
+)
+
+# Advanced configuration with custom httpx settings
+import httpx
+
+client = ForecastClient(
+ api_key="your-api-key",
timeout=120.0,
- verify_ssl=True
+ limits=httpx.Limits(max_connections=10), # Connection pooling
+ headers={"X-Custom-Header": "value"} # Custom headers
)
```
### Request Options
```python
+# Compression options for large payloads
+request = Chronos2ForecastRequest(
+ x=data,
+ horizon=24,
+ compression="zstd" # Options: "zstd", "lz4", None (default: "zstd")
+)
+
+# Model version pinning
request = FlowStateForecastRequest(
x=data,
- horizon=10,
- model_version="1",
- compression="zstd", # Options: "zstd", "lz4", None
+ horizon=24,
+ model_version="1.2.3" # Pin to specific version (default: "latest")
)
```
+## Context Managers
+
+Use context managers for automatic resource cleanup:
+
+```python
+# Sync context manager
+with ForecastClient() as client:
+ request = Chronos2ForecastRequest(x=data, horizon=24, quantiles=[0.1, 0.5, 0.9])
+ response = client.forecast(request)
+ print(response.quantiles)
+# Client automatically closed
+
+# Async context manager
+async with ForecastClient() as client:
+ request = Chronos2ForecastRequest(x=data, horizon=24, quantiles=[0.1, 0.9])
+ response = await client.forecast_async(request)
+ print(response.quantiles)
+# Client automatically closed
+```
+
+## Examples
+
+See the `examples/` directory for complete Jupyter notebook examples:
+
+- **`flowstate_simple_example.ipynb`** - Point forecasting with FlowState on AirPassengers dataset
+- **`chronos2_probabilistic.ipynb`** - Probabilistic forecasting with quantiles (coming soon)
+- **`batch_processing.ipynb`** - Efficient batch processing patterns (coming soon)
+
## Requirements
-- Python >= 3.10
-- numpy >= 1.20.0
-- pyarrow >= 10.0.0
+- Python >= 3.11
+- numpy >= 1.26.0
+- pyarrow >= 11.0.0
- httpx >= 0.23.0
+- pydantic >= 2.0.0
-## Development
+## Performance Tips
-### Regenerating the Low-Level Client
+1. **Batch Processing**: Process multiple time series in a single request for optimal throughput
+ ```python
+ # Good: Single request with 32 series
+ data = np.random.randn(32, 100, 1)
-The `faim_client` package is auto-generated from the OpenAPI specification. To regenerate after API changes:
+ # Less efficient: 32 separate requests
+ # for series in data: client.forecast(...)
+ ```
-```bash
-# Get the latest OpenAPI spec from your server
-curl http://your-server:8003/openapi.json > openapi.json
+2. **Compression**: Use `compression="zstd"` for large payloads (default, recommended)
-# Regenerate the client
-openapi-python-client generate --path openapi.json --config client.config.yaml --meta none
-```
+3. **Async for Concurrent Requests**: Use `forecast_async()` with `asyncio.gather()` for parallel processing
-The `--meta none` flag prevents creating an extra outer directory.
+4. **Connection Pooling**: Reuse client instances across requests instead of creating new ones
-After regenerating, test that `faim_sdk` still works correctly:
-```bash
-poetry install
-pytest tests/
-```
+## Support
+
+- **Documentation**: [docs.faim.example.com](https://docs.faim.example.com)
+- **Issues**: [GitHub Issues](https://github.com/your-org/faim-sdk/issues)
+- **Email**: support@faim.example.com
## License
-Apache 2.0
\ No newline at end of file
+Apache License 2.0 - See [LICENSE](LICENSE) file for details.
+
+## Citation
+
+If you use FAIM in your research, please cite:
+
+```bibtex
+@software{faim_sdk,
+ title = {FAIM SDK: Foundation AI Models for Time Series Forecasting},
+ author = {FAIM Team},
+ year = {2024},
+ url = {https://github.com/your-org/faim-sdk}
+}
+```
diff --git a/examples/flowstate_simple_example.ipynb b/examples/flowstate_simple_example.ipynb
deleted file mode 100644
index 7f7850c..0000000
--- a/examples/flowstate_simple_example.ipynb
+++ /dev/null
@@ -1,686 +0,0 @@
-{
- "cells": [
- {
- "cell_type": "markdown",
- "metadata": {},
- "source": "# FAIM SDK Example: FlowState Forecasting with Sunspots Dataset\n\nThis notebook demonstrates how to use the FAIM SDK to forecast time-series data using the FlowState model.\n\n**Dataset**: Sunspots - Annual sunspot activity observations (1700-2008)"
- },
- {
- "cell_type": "markdown",
- "metadata": {},
- "source": [
- "## 1. Setup and Installation"
- ]
- },
- {
- "cell_type": "code",
- "metadata": {
- "ExecuteTime": {
- "end_time": "2025-10-30T16:19:55.580574Z",
- "start_time": "2025-10-30T16:19:55.572729Z"
- }
- },
- "source": [
- "# Install dependencies if needed\n",
- "# !pip install numpy pandas matplotlib statsmodels"
- ],
- "outputs": [],
- "execution_count": 60
- },
- {
- "cell_type": "code",
- "metadata": {
- "ExecuteTime": {
- "end_time": "2025-10-30T16:19:55.594776Z",
- "start_time": "2025-10-30T16:19:55.592104Z"
- }
- },
- "source": [
- "import os\n",
- "\n",
- "import matplotlib.pyplot as plt\n",
- "import numpy as np\n",
- "import pandas as pd\n",
- "\n",
- "from faim_client.models import ModelName\n",
- "\n",
- "# Import FAIM SDK\n",
- "from faim_sdk import FlowStateForecastRequest, ForecastClient"
- ],
- "outputs": [],
- "execution_count": 61
- },
- {
- "cell_type": "markdown",
- "metadata": {},
- "source": "## 2. Load Sunspots Dataset\n\nThe Sunspots dataset is built into statsmodels - no downloads needed!"
- },
- {
- "cell_type": "code",
- "metadata": {
- "ExecuteTime": {
- "end_time": "2025-10-30T16:19:55.637833Z",
- "start_time": "2025-10-30T16:19:55.620782Z"
- }
- },
- "source": [
- "# Load Sunspots dataset from statsmodels\n",
- "import statsmodels.api as sm\n",
- "\n",
- "# Load the dataset\n",
- "sunspots_data = sm.datasets.sunspots.load_pandas()\n",
- "df = sunspots_data.data\n",
- "\n",
- "print(\"โ Dataset loaded successfully\")\n",
- "print(\" Dataset: Yearly sunspots (1700-2008)\")\n",
- "print(f\" Shape: {df.shape}\")\n",
- "print(\"\\nDataset preview:\")\n",
- "print(df.head())\n",
- "\n",
- "# Display basic info\n",
- "print(f\"\\nColumns: {df.columns.tolist()}\")\n",
- "print(f\"Data types:\\n{df.dtypes}\")"
- ],
- "outputs": [
- {
- "name": "stdout",
- "output_type": "stream",
- "text": [
- "โ Dataset loaded successfully\n",
- " Dataset: Yearly sunspots (1700-2008)\n",
- " Shape: (309, 2)\n",
- "\n",
- "Dataset preview:\n",
- " YEAR SUNACTIVITY\n",
- "0 1700.0 5.0\n",
- "1 1701.0 11.0\n",
- "2 1702.0 16.0\n",
- "3 1703.0 23.0\n",
- "4 1704.0 36.0\n",
- "\n",
- "Columns: ['YEAR', 'SUNACTIVITY']\n",
- "Data types:\n",
- "YEAR float64\n",
- "SUNACTIVITY float64\n",
- "dtype: object\n"
- ]
- }
- ],
- "execution_count": 62
- },
- {
- "cell_type": "code",
- "metadata": {
- "ExecuteTime": {
- "end_time": "2025-10-30T16:19:55.666166Z",
- "start_time": "2025-10-30T16:19:55.660954Z"
- }
- },
- "source": [
- "df.head()"
- ],
- "outputs": [
- {
- "data": {
- "text/plain": [
- " YEAR SUNACTIVITY\n",
- "0 1700.0 5.0\n",
- "1 1701.0 11.0\n",
- "2 1702.0 16.0\n",
- "3 1703.0 23.0\n",
- "4 1704.0 36.0"
- ],
- "text/html": [
- "
\n",
- "\n",
- "
\n",
- " \n",
- " \n",
- " | \n",
- " YEAR | \n",
- " SUNACTIVITY | \n",
- "
\n",
- " \n",
- " \n",
- " \n",
- " | 0 | \n",
- " 1700.0 | \n",
- " 5.0 | \n",
- "
\n",
- " \n",
- " | 1 | \n",
- " 1701.0 | \n",
- " 11.0 | \n",
- "
\n",
- " \n",
- " | 2 | \n",
- " 1702.0 | \n",
- " 16.0 | \n",
- "
\n",
- " \n",
- " | 3 | \n",
- " 1703.0 | \n",
- " 23.0 | \n",
- "
\n",
- " \n",
- " | 4 | \n",
- " 1704.0 | \n",
- " 36.0 | \n",
- "
\n",
- " \n",
- "
\n",
- "
"
- ]
- },
- "execution_count": 63,
- "metadata": {},
- "output_type": "execute_result"
- }
- ],
- "execution_count": 63
- },
- {
- "cell_type": "code",
- "metadata": {
- "ExecuteTime": {
- "end_time": "2025-10-30T16:19:55.729968Z",
- "start_time": "2025-10-30T16:19:55.716535Z"
- }
- },
- "source": [
- "# Inspect the dataset\n",
- "print(f\"Dataset shape: {df.shape}\")\n",
- "print(f\"Columns: {df.columns.tolist()}\")\n",
- "print(\"\\nBasic statistics:\")\n",
- "print(df.describe())"
- ],
- "outputs": [
- {
- "name": "stdout",
- "output_type": "stream",
- "text": [
- "Dataset shape: (309, 2)\n",
- "Columns: ['YEAR', 'SUNACTIVITY']\n",
- "\n",
- "Basic statistics:\n",
- " YEAR SUNACTIVITY\n",
- "count 309.000000 309.000000\n",
- "mean 1854.000000 49.752104\n",
- "std 89.344838 40.452595\n",
- "min 1700.000000 0.000000\n",
- "25% 1777.000000 16.000000\n",
- "50% 1854.000000 40.000000\n",
- "75% 1931.000000 69.800000\n",
- "max 2008.000000 190.200000\n"
- ]
- }
- ],
- "execution_count": 64
- },
- {
- "cell_type": "markdown",
- "metadata": {},
- "source": [
- "## 3. Data Preprocessing"
- ]
- },
- {
- "cell_type": "code",
- "metadata": {
- "ExecuteTime": {
- "end_time": "2025-10-30T16:19:55.994396Z",
- "start_time": "2025-10-30T16:19:55.906651Z"
- }
- },
- "source": [
- "# Extract sunspot activity as numpy array\n",
- "# Column name is 'SUNACTIVITY' in sunspots dataset\n",
- "sunspots = df[\"SUNACTIVITY\"].values.astype(np.float32)\n",
- "\n",
- "print(f\"Time series length: {len(sunspots)}\")\n",
- "print(f\"Value range: [{sunspots.min():.1f}, {sunspots.max():.1f}]\")\n",
- "print(\"Period: 1700-2008 (annual observations)\")\n",
- "\n",
- "# Visualize the time series\n",
- "plt.figure(figsize=(14, 4))\n",
- "plt.plot(sunspots)\n",
- "plt.title(\"Sunspot Activity Time Series (1700-2008)\", fontsize=14)\n",
- "plt.xlabel(\"Year Index\")\n",
- "plt.ylabel(\"Sunspot Activity\")\n",
- "plt.grid(True, alpha=0.3)\n",
- "plt.tight_layout()\n",
- "plt.show()"
- ],
- "outputs": [
- {
- "name": "stdout",
- "output_type": "stream",
- "text": [
- "Time series length: 309\n",
- "Value range: [0.0, 190.2]\n",
- "Period: 1700-2008 (annual observations)\n"
- ]
- },
- {
- "data": {
- "text/plain": [
- ""
- ],
- "image/png": "iVBORw0KGgoAAAANSUhEUgAABW0AAAGGCAYAAAAAW6PhAAAAOnRFWHRTb2Z0d2FyZQBNYXRwbG90bGliIHZlcnNpb24zLjEwLjcsIGh0dHBzOi8vbWF0cGxvdGxpYi5vcmcvTLEjVAAAAAlwSFlzAAAPYQAAD2EBqD+naQABAABJREFUeJzsnQeYZFWZ/k/nnOP0TE9OpBlykpwUBEkmxBUVBXdVFAwr6qoIK0b+JmQNCKKyIOgCoqI4AkOGYRgGmGFyns45x/o/37n3u3WrusIN54bufn/PU1M9HapO3XjOe97zfhmRSCQiAAAAAAAAAAAAAAAAAISCzKAbAAAAAAAAAAAAAAAAACAKRFsAAAAAAAAAAAAAAAAIERBtAQAAAAAAAAAAAAAAIERAtAUAAAAAAAAAAAAAAIAQAdEWAAAAAAAAAAAAAAAAQgREWwAAAAAAAAAAAAAAAAgREG0BAAAAAAAAAAAAAAAgREC0BQAAAAAAAAAAAAAAgBAB0RYAAAAAAAAAAAAAAABCBERbAAAAAIBZwje+8Q2RkZEhnnzyyUBfww+ofdROai9Ize7du+W2+vCHPyzCwODgoJg7d6645pprgm4KsMmWLVtEdna2+NnPfhZ0UwAAAIBpD0RbAAAAACRlYGBAfOtb3xJHH320KC4uFnl5eWLevHni1FNPFTfeeKPYsWOHmK0sXLhQPtxwzz33SLGMHi+//PK0FSq9Fv1YKLb6mI5C7bPPPive8573SLEyNzdXVFRUiJUrV4oPfOAD4je/+Y2YTXzve98T7e3t4qtf/WrM9zds2CC+/OUvi7e//e2ipqZG7uszzjgj6evQ8ZjuWLn55pun/N3WrVvFe9/7XlFdXS0KCgrE6tWrxR133CEikUjC9+nt7RU33HCDWLBggbxG0nXhC1/4gujv77f1uenz/dd//Zc48cQTRW1trXytxYsXi//4j/8QBw4cSPp3Xre3u7tbfO1rXxOrVq0SJSUl8n2OO+448dOf/lQMDw/H/O6KFSvEFVdcIW666SbR19dn6/MDAAAAIJbsuP8DAAAAAEhowH3KKaeIjRs3iqVLl4oPfvCDoqqqSoopL730kvj2t78tlixZIh/AGXfeeacUjkhc+fWvfy2FEC/51Kc+Jd7//veL+fPnB/oadkkkzJHA9fDDD4vTTz99ys/p/8cff7zYvHmzFJjCzt133y0++tGPSofiBRdcIJYtWyaPC3It/vWvfxVr164VV111lWfvT0IxbauysjIRNCQofv/73xfve9/7phxjDz30kLj11lulqL18+XJ5LUrFJZdcknRihd6DJqVIADazadMmcfLJJ4uhoSEphDY0NIi//OUvUjiln/3kJz+J+X16DToG6Xg877zzpGD56quvytd/6qmn5L7Lz8+39Nk/8YlPiBdffFEeu3SOkaBK/ycB9oEHHhBPP/20FPL9bC8Jtsccc4zYuXOnvB9ce+21YmRkRPztb38Tn/70p8X//d//iccff1xkZka9QF/84hfF7373O/HjH/9YfOUrX7H02QEAAACQgAgAAAAAQAK++c1vkk0r8rGPfSwyOTk55ec7d+6MbN68OTJbWbBggXw4ZevWrXL7vutd74osX748UlZWFhkcHHTVpieeeEK+5te//vWIn+zatUu+71VXXeXbe951112BfFbVDAwMREpKSiKlpaWRN954Y8rPR0dHI//4xz8is4Wf/vSncr8+/vjjU35G2+eVV16R26SpqUn+3umnn277PdatWyf/9ogjjpjys9NOO03+7K9//avxvZGRkcipp54qv//cc8/F/P7XvvY1+f3//M//jPk+/Z++/61vfctyu3784x9Htm3bNuX73/72t+VrXXDBBb639zvf+Y78/mc/+9mY79N7HHvssfJnTz311JR2rVq1Sl4fJyYmLH9+AAAAAMQC0RYAAAAACTn//PPlgPzVV1+1/DepRJREIieJfPQ3JAD/6Ec/iqxYsSKSm5sbmT9/fuQb3/jGlAE//f+Xv/xl5LjjjotUVFRE8vPzI3Pnzo1ceOGFUrBMJF4+/fTTsk3FxcVSGL3ssssSCiPE66+/HnnPe94Tqampke1YuHBh5DOf+Uykvb19ikCZ6GFHQPzSl74k/+bBBx+M3HzzzfLre+65J+nv79ixI/Lxj39ctonaRm2kz0XiJUHvnaxd1Gbz7/C22r17dyQjIyNy5plnJnxPEseqqqoi8+bNM/ZF/GuweJroQb/zla98RX59//33J3yPO++807a4ZX7fZNs8mYDNx2F3d3fkE5/4RKS+vj5SWFgoRS4SBIkDBw5ErrzySrmN6Rg799xzpcieCDp2r7766khjY6PcL/R6dFzTtrXCiy++KNt58cUXR+zy0EMPRc4666xIeXl5JC8v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"
- },
- "metadata": {},
- "output_type": "display_data"
- }
- ],
- "execution_count": 65
- },
- {
- "cell_type": "code",
- "metadata": {
- "ExecuteTime": {
- "end_time": "2025-10-30T16:19:56.025546Z",
- "start_time": "2025-10-30T16:19:56.022577Z"
- }
- },
- "source": [
- "# Prepare data for forecasting\n",
- "# FlowState expects shape: (batch_size, sequence_length, features)\n",
- "# For univariate time series, features = 1\n",
- "\n",
- "# Use last 200 years for context, forecast next 10 years\n",
- "context_length = 250\n",
- "horizon = 20\n",
- "\n",
- "# Use data from index -110 to -10 for context\n",
- "x = sunspots[-context_length - horizon : -horizon].reshape(1, context_length, 1)\n",
- "\n",
- "# Ground truth for comparison (last 10 years)\n",
- "y_true = sunspots[-horizon:]\n",
- "\n",
- "print(f\"Input shape: {x.shape}\")\n",
- "print(f\"Context period: {context_length} years\")\n",
- "print(f\"Forecasting {horizon} years ahead\")\n",
- "print(f\"Ground truth available: {len(y_true)} points\")"
- ],
- "outputs": [
- {
- "name": "stdout",
- "output_type": "stream",
- "text": [
- "Input shape: (1, 250, 1)\n",
- "Context period: 250 years\n",
- "Forecasting 20 years ahead\n",
- "Ground truth available: 20 points\n"
- ]
- }
- ],
- "execution_count": 66
- },
- {
- "cell_type": "markdown",
- "metadata": {},
- "source": [
- "## 4. Initialize FAIM Client\n",
- "\n",
- "**Note**: Replace `base_url` with your actual FAIM inference API endpoint."
- ]
- },
- {
- "cell_type": "code",
- "metadata": {
- "ExecuteTime": {
- "end_time": "2025-10-30T16:19:56.053034Z",
- "start_time": "2025-10-30T16:19:56.043036Z"
- }
- },
- "source": [
- "# Initialize the client\n",
- "# TODO: Replace with your actual FAIM API endpoint and API key\n",
- "# BASE_URL = \"http://localhost:8003\" # Example: \"https://api.faim.example.com\"\n",
- "# BASE_URL = (\"http://34.141.187.117\") # Example: \"https://api.faim.example.com\"\n",
- "BASE_URL = (\"https://api.faim.it.com\") # Example: \"https://api.faim.example.com\"\n",
- "FAIM_API_KEY = os.environ[\"FAIM_API_KEY\"] # \"your-secret-api-key\"\n",
- "client = ForecastClient(\n",
- " base_url=BASE_URL,\n",
- " api_key=FAIM_API_KEY, # Optional: API key for authentication\n",
- " timeout=60.0, # 60 seconds timeout\n",
- " verify_ssl=True,\n",
- ")\n",
- "\n",
- "print(f\"โ ForecastClient initialized: {BASE_URL}\")\n",
- "if FAIM_API_KEY:\n",
- " print(\" Authentication: Enabled\")\n",
- "else:\n",
- " print(\" Authentication: Disabled\")\n",
- " assert False, 'you need to set up your API key'"
- ],
- "outputs": [
- {
- "name": "stderr",
- "output_type": "stream",
- "text": [
- "INFO:faim_sdk.client:Initialized ForecastClient with authentication: base_url=https://api.faim.it.com, timeout=60.0s\n"
- ]
- },
- {
- "name": "stdout",
- "output_type": "stream",
- "text": [
- "โ ForecastClient initialized: https://api.faim.it.com\n",
- " Authentication: Enabled\n"
- ]
- }
- ],
- "execution_count": 67
- },
- {
- "cell_type": "code",
- "metadata": {
- "ExecuteTime": {
- "end_time": "2025-10-30T16:19:56.105543Z",
- "start_time": "2025-10-30T16:19:56.099754Z"
- }
- },
- "source": [
- "import logging\n",
- "\n",
- "# Enable debug logging\n",
- "logging.basicConfig(level=logging.DEBUG)\n"
- ],
- "outputs": [],
- "execution_count": 68
- },
- {
- "cell_type": "markdown",
- "metadata": {},
- "source": "# Create FlowState forecast request\nrequest = FlowStateForecastRequest(\n x=x,\n horizon=horizon,\n model_version=\"1\", # Use version 1 (backend requires integer versions)\n scale_factor=1.0, # Optional: normalization factor\n prediction_type=\"point\", # Optional: prediction mode\n compression=\"zstd\" # Use compression for efficiency\n)\n\nprint(f\"Request created:\")\nprint(f\" - Input shape: {request.x.shape}\")\nprint(f\" - Horizon: {request.horizon}\")\nprint(f\" - Model version: {request.model_version}\")\nprint(f\" - Compression: {request.compression}\")"
- },
- {
- "cell_type": "code",
- "metadata": {
- "ExecuteTime": {
- "end_time": "2025-10-30T16:19:56.133897Z",
- "start_time": "2025-10-30T16:19:56.131085Z"
- }
- },
- "source": [
- "# Create FlowState forecast request\n",
- "request = FlowStateForecastRequest(\n",
- " x=x,\n",
- " horizon=horizon, # or specific version like \"1.0\"\n",
- " scale_factor=0.65, # Optional: normalization factor\n",
- " prediction_type=\"mean\", # Optional: prediction mode\n",
- " compression=None\n",
- " # compression=\"zstd\", # Use compression for efficiency\n",
- ")\n",
- "\n",
- "print(\"Request created:\")\n",
- "print(f\" - Input shape: {request.x.shape}\")\n",
- "print(f\" - Horizon: {request.horizon}\")\n",
- "print(f\" - Model version: {request.model_version}\")\n",
- "print(f\" - Compression: {request.compression}\")"
- ],
- "outputs": [
- {
- "name": "stdout",
- "output_type": "stream",
- "text": [
- "Request created:\n",
- " - Input shape: (1, 250, 1)\n",
- " - Horizon: 20\n",
- " - Model version: 1\n",
- " - Compression: None\n"
- ]
- }
- ],
- "execution_count": 69
- },
- {
- "cell_type": "markdown",
- "metadata": {},
- "source": [
- "## 6. Generate Forecast"
- ]
- },
- {
- "cell_type": "code",
- "metadata": {
- "ExecuteTime": {
- "end_time": "2025-10-30T16:19:58.648828Z",
- "start_time": "2025-10-30T16:19:56.151677Z"
- }
- },
- "source": [
- "try:\n",
- " # Make forecast request\n",
- " response = client.forecast(ModelName.FLOWSTATE, request)\n",
- "\n",
- " print(\"โ Forecast successful!\")\n",
- " print(f\"\\nResponse: {response}\")\n",
- " print(\"\\nResponse details:\")\n",
- " print(f\" - Point predictions shape: {response.point.shape}\")\n",
- " print(f\" - Metadata: {response.metadata}\")\n",
- "\n",
- " # Extract point predictions (FlowState returns 'point' array)\n",
- " predictions = response.point.flatten() # Shape: (horizon,)\n",
- "\n",
- "except Exception as e:\n",
- " print(f\"โ Forecast failed: {e}\")\n",
- " print(f\"\\nError type: {type(e).__name__}\")\n",
- " if hasattr(e, \"details\"):\n",
- " print(f\"Details: {e.details}\")\n",
- " raise"
- ],
- "outputs": [
- {
- "name": "stderr",
- "output_type": "stream",
- "text": [
- "DEBUG:faim_sdk.client:Starting forecast: model=flowstate, version=1, x.shape=(1, 250, 1), horizon=20\n",
- "DEBUG:faim_sdk.client:Serialized request: 1520 bytes, metadata={'horizon': 20, 'output_type': 'point', 'scale_factor': 0.65, 'prediction_type': 'mean'}\n",
- "DEBUG:httpcore.connection:connect_tcp.started host='api.faim.it.com' port=443 local_address=None timeout=60.0 socket_options=None\n",
- "DEBUG:httpcore.connection:connect_tcp.complete return_value=\n",
- "DEBUG:httpcore.connection:start_tls.started ssl_context= server_hostname='api.faim.it.com' timeout=60.0\n",
- "DEBUG:httpcore.connection:start_tls.complete return_value=\n",
- "DEBUG:httpcore.http11:send_request_headers.started request=\n",
- "DEBUG:httpcore.http11:send_request_headers.complete\n",
- "DEBUG:httpcore.http11:send_request_body.started request=\n",
- "DEBUG:httpcore.http11:send_request_body.complete\n",
- "DEBUG:httpcore.http11:receive_response_headers.started request=\n",
- "DEBUG:httpcore.http11:receive_response_headers.complete return_value=(b'HTTP/1.1', 401, b'Unauthorized', [(b'date', b'Thu, 30 Oct 2025 16:19:57 GMT'), (b'server', b'uvicorn'), (b'Content-Length', b'74'), (b'content-type', b'application/json'), (b'Via', b'1.1 google'), (b'Alt-Svc', b'h3=\":443\"; ma=2592000,h3-29=\":443\"; ma=2592000')])\n",
- "INFO:httpx:HTTP Request: POST https://api.faim.it.com/v1/ts/forecast/flowstate/1 \"HTTP/1.1 401 Unauthorized\"\n",
- "DEBUG:httpcore.http11:receive_response_body.started request=\n",
- "DEBUG:httpcore.http11:receive_response_body.complete\n",
- "DEBUG:httpcore.http11:response_closed.started\n",
- "DEBUG:httpcore.http11:response_closed.complete\n",
- "ERROR:faim_sdk.client:Unexpected status code: 401\n"
- ]
- },
- {
- "name": "stdout",
- "output_type": "stream",
- "text": [
- "โ Forecast failed: Unexpected status code: 401 | status=401\n",
- "\n",
- "Error type: APIError\n",
- "Details: {}\n"
- ]
- },
- {
- "ename": "APIError",
- "evalue": "Unexpected status code: 401 | status=401",
- "output_type": "error",
- "traceback": [
- "\u001B[0;31m---------------------------------------------------------------------------\u001B[0m",
- "\u001B[0;31mAPIError\u001B[0m Traceback (most recent call last)",
- "Cell \u001B[0;32mIn[70], line 3\u001B[0m\n\u001B[1;32m 1\u001B[0m \u001B[38;5;28;01mtry\u001B[39;00m:\n\u001B[1;32m 2\u001B[0m \u001B[38;5;66;03m# Make forecast request\u001B[39;00m\n\u001B[0;32m----> 3\u001B[0m response \u001B[38;5;241m=\u001B[39m \u001B[43mclient\u001B[49m\u001B[38;5;241;43m.\u001B[39;49m\u001B[43mforecast\u001B[49m\u001B[43m(\u001B[49m\u001B[43mModelName\u001B[49m\u001B[38;5;241;43m.\u001B[39;49m\u001B[43mFLOWSTATE\u001B[49m\u001B[43m,\u001B[49m\u001B[43m \u001B[49m\u001B[43mrequest\u001B[49m\u001B[43m)\u001B[49m\n\u001B[1;32m 5\u001B[0m \u001B[38;5;28mprint\u001B[39m(\u001B[38;5;124m\"\u001B[39m\u001B[38;5;124mโ Forecast successful!\u001B[39m\u001B[38;5;124m\"\u001B[39m)\n\u001B[1;32m 6\u001B[0m \u001B[38;5;28mprint\u001B[39m(\u001B[38;5;124mf\u001B[39m\u001B[38;5;124m\"\u001B[39m\u001B[38;5;130;01m\\n\u001B[39;00m\u001B[38;5;124mResponse: \u001B[39m\u001B[38;5;132;01m{\u001B[39;00mresponse\u001B[38;5;132;01m}\u001B[39;00m\u001B[38;5;124m\"\u001B[39m)\n",
- "File \u001B[0;32m~/Documents/Personal/research/FAIM/faim-client/faim_sdk/client.py:225\u001B[0m, in \u001B[0;36mForecastClient.forecast\u001B[0;34m(self, model, request)\u001B[0m\n\u001B[1;32m 223\u001B[0m \u001B[38;5;28;01melif\u001B[39;00m response\u001B[38;5;241m.\u001B[39mstatus_code \u001B[38;5;241m!=\u001B[39m \u001B[38;5;241m200\u001B[39m:\n\u001B[1;32m 224\u001B[0m logger\u001B[38;5;241m.\u001B[39merror(\u001B[38;5;124mf\u001B[39m\u001B[38;5;124m\"\u001B[39m\u001B[38;5;124mUnexpected status code: \u001B[39m\u001B[38;5;132;01m{\u001B[39;00mresponse\u001B[38;5;241m.\u001B[39mstatus_code\u001B[38;5;132;01m}\u001B[39;00m\u001B[38;5;124m\"\u001B[39m)\n\u001B[0;32m--> 225\u001B[0m \u001B[38;5;28;01mraise\u001B[39;00m APIError(\n\u001B[1;32m 226\u001B[0m \u001B[38;5;124mf\u001B[39m\u001B[38;5;124m\"\u001B[39m\u001B[38;5;124mUnexpected status code: \u001B[39m\u001B[38;5;132;01m{\u001B[39;00mresponse\u001B[38;5;241m.\u001B[39mstatus_code\u001B[38;5;132;01m}\u001B[39;00m\u001B[38;5;124m\"\u001B[39m,\n\u001B[1;32m 227\u001B[0m status_code\u001B[38;5;241m=\u001B[39mresponse\u001B[38;5;241m.\u001B[39mstatus_code,\n\u001B[1;32m 228\u001B[0m response\u001B[38;5;241m=\u001B[39mresponse\u001B[38;5;241m.\u001B[39mparsed \u001B[38;5;28;01mif\u001B[39;00m \u001B[38;5;28mhasattr\u001B[39m(response, \u001B[38;5;124m\"\u001B[39m\u001B[38;5;124mparsed\u001B[39m\u001B[38;5;124m\"\u001B[39m) \u001B[38;5;28;01melse\u001B[39;00m \u001B[38;5;28;01mNone\u001B[39;00m,\n\u001B[1;32m 229\u001B[0m )\n\u001B[1;32m 231\u001B[0m \u001B[38;5;66;03m# Deserialize successful response\u001B[39;00m\n\u001B[1;32m 232\u001B[0m \u001B[38;5;28;01mtry\u001B[39;00m:\n",
- "\u001B[0;31mAPIError\u001B[0m: Unexpected status code: 401 | status=401"
- ]
- }
- ],
- "execution_count": 70
- },
- {
- "cell_type": "markdown",
- "metadata": {},
- "source": [
- "## 7. Visualize Results"
- ]
- },
- {
- "cell_type": "code",
- "metadata": {},
- "source": [
- "# Plot forecast vs ground truth\n",
- "plt.figure(figsize=(14, 6))\n",
- "\n",
- "# Historical data (context) - last 100 years before forecast\n",
- "historical_start = len(sunspots) - context_length - horizon\n",
- "plt.plot(\n",
- " range(historical_start, historical_start + context_length),\n",
- " sunspots[-context_length - horizon : -horizon],\n",
- " label=\"Historical Context\",\n",
- " color=\"blue\",\n",
- " linewidth=2,\n",
- ")\n",
- "\n",
- "# Ground truth (actual future values - last 10 years)\n",
- "plt.plot(\n",
- " range(historical_start + context_length, historical_start + context_length + horizon),\n",
- " y_true,\n",
- " label=\"Ground Truth\",\n",
- " color=\"green\",\n",
- " linewidth=2,\n",
- " linestyle=\"--\",\n",
- ")\n",
- "\n",
- "# Predictions\n",
- "plt.plot(\n",
- " range(historical_start + context_length, historical_start + context_length + horizon),\n",
- " predictions,\n",
- " label=\"FlowState Forecast\",\n",
- " color=\"red\",\n",
- " linewidth=2,\n",
- " marker=\"o\",\n",
- ")\n",
- "\n",
- "# Formatting\n",
- "plt.axvline(x=historical_start + context_length, color=\"gray\", linestyle=\":\", alpha=0.5, label=\"Forecast Start\")\n",
- "plt.title(\"Sunspot Activity Forecast: FlowState Model\", fontsize=14, fontweight=\"bold\")\n",
- "plt.xlabel(\"Year Index\", fontsize=12)\n",
- "plt.ylabel(\"Sunspot Activity\", fontsize=12)\n",
- "plt.legend(loc=\"upper left\", fontsize=10)\n",
- "plt.grid(True, alpha=0.3)\n",
- "plt.tight_layout()\n",
- "plt.show()"
- ],
- "outputs": [],
- "execution_count": null
- },
- {
- "cell_type": "markdown",
- "metadata": {},
- "source": [
- "## 8. Evaluate Forecast Accuracy"
- ]
- },
- {
- "cell_type": "code",
- "metadata": {},
- "source": [
- "# Calculate error metrics\n",
- "mae = np.mean(np.abs(predictions - y_true))\n",
- "rmse = np.sqrt(np.mean((predictions - y_true) ** 2))\n",
- "mape = np.mean(np.abs((predictions - y_true) / (y_true + 1e-8))) * 100 # Add small epsilon to avoid division by zero\n",
- "\n",
- "print(\"Forecast Accuracy Metrics:\")\n",
- "print(f\" MAE (Mean Absolute Error): {mae:.2f}\")\n",
- "print(f\" RMSE (Root Mean Squared Error): {rmse:.2f}\")\n",
- "print(f\" MAPE (Mean Absolute % Error): {mape:.2f}%\")\n",
- "\n",
- "# Show point-by-point comparison\n",
- "comparison_df = pd.DataFrame(\n",
- " {\n",
- " \"Year\": range(1, horizon + 1),\n",
- " \"Ground Truth\": y_true,\n",
- " \"Predicted\": predictions,\n",
- " \"Error\": predictions - y_true,\n",
- " \"Abs Error\": np.abs(predictions - y_true),\n",
- " \"Error %\": np.abs((predictions - y_true) / (y_true + 1e-8)) * 100,\n",
- " }\n",
- ")\n",
- "\n",
- "print(\"\\nPoint-by-point comparison:\")\n",
- "print(comparison_df.to_string(index=False))"
- ],
- "outputs": [],
- "execution_count": null
- },
- {
- "cell_type": "markdown",
- "metadata": {},
- "source": [
- "## 9. Cleanup"
- ]
- },
- {
- "cell_type": "code",
- "metadata": {},
- "source": [
- "# Close the client connection\n",
- "# client.close()\n",
- "# print(\"โ Client connection closed\")"
- ],
- "outputs": [],
- "execution_count": null
- },
- {
- "cell_type": "markdown",
- "metadata": {},
- "source": [
- "## Next Steps\n",
- "\n",
- "Try experimenting with:\n",
- "- Different `context_length` values\n",
- "- Different `horizon` values\n",
- "- Different `scale_factor` for normalization\n",
- "- The ToTo model with `ToToForecastRequest` for probabilistic forecasts\n",
- "- Multiple time series in a batch (increase batch_size dimension)"
- ]
- }
- ],
- "metadata": {
- "kernelspec": {
- "display_name": "Python 3",
- "language": "python",
- "name": "python3"
- },
- "language_info": {
- "codemirror_mode": {
- "name": "ipython",
- "version": 3
- },
- "file_extension": ".py",
- "mimetype": "text/x-python",
- "name": "python",
- "nbconvert_exporter": "python",
- "pygments_lexer": "ipython3",
- "version": "3.10.0"
- }
- },
- "nbformat": 4,
- "nbformat_minor": 4
-}
diff --git a/examples/generate_data.py b/examples/generate_data.py
new file mode 100644
index 0000000..0cf3697
--- /dev/null
+++ b/examples/generate_data.py
@@ -0,0 +1,250 @@
+"""
+Synthetic time series data generation for FAIM SDK examples.
+
+This module provides functions to generate synthetic time series data with clear patterns
+for testing and demonstrating forecasting capabilities.
+"""
+
+import numpy as np
+
+
+def generate_linear_trend_series(
+ batch_size: int = 1,
+ context_length: int = 256,
+ trend_slope: float = 0.05,
+ noise_std: float = 0.5,
+ seed: int | None = None,
+) -> np.ndarray:
+ """
+ Generate simple time series with linear trend and Gaussian noise.
+
+ Pattern: y(t) = trend_slope * t + noise
+ This creates an easily forecastable upward or downward trend.
+
+ Args:
+ batch_size: Number of independent time series to generate
+ context_length: Length of each time series
+ trend_slope: Slope of the linear trend (positive for upward, negative for downward)
+ noise_std: Standard deviation of Gaussian noise
+ seed: Random seed for reproducibility
+
+ Returns:
+ np.ndarray: Shape (batch_size, context_length, 1)
+ """
+ if seed is not None:
+ np.random.seed(seed)
+
+ # Generate time indices
+ t = np.arange(context_length)
+
+ # Create linear trend
+ trend = trend_slope * t
+
+ # Add Gaussian noise
+ noise = np.random.normal(0, noise_std, size=(batch_size, context_length))
+
+ # Combine trend and noise
+ series = trend[np.newaxis, :] + noise
+
+ # Reshape to (batch_size, context_length, 1)
+ return series[:, :, np.newaxis]
+
+
+def generate_correlated_multi_series(
+ batch_size: int = 1,
+ context_length: int = 256,
+ correlation: float = 0.8,
+ seasonal_period: int = 24,
+ seasonal_amplitude: float = 2.0,
+ trend_slope: float = 0.02,
+ noise_std: float = 0.3,
+ seed: int | None = None,
+) -> np.ndarray:
+ """
+ Generate two correlated time series with seasonal patterns.
+
+ Pattern:
+ - Series 1: trend + seasonality + noise
+ - Series 2: correlation * Series1 + (1-correlation) * independent_pattern + noise
+
+ This creates two related time series where forecasting one can help predict the other.
+
+ Args:
+ batch_size: Number of independent time series pairs to generate
+ context_length: Length of each time series
+ correlation: Correlation coefficient between the two series (0 to 1)
+ seasonal_period: Period of seasonal component (e.g., 24 for daily pattern)
+ seasonal_amplitude: Amplitude of seasonal oscillation
+ trend_slope: Slope of the linear trend
+ noise_std: Standard deviation of Gaussian noise
+ seed: Random seed for reproducibility
+
+ Returns:
+ np.ndarray: Shape (batch_size, context_length, 2) - two correlated features
+ """
+ if seed is not None:
+ np.random.seed(seed)
+
+ # Generate time indices
+ t = np.arange(context_length)
+
+ # Create shared components
+ trend = trend_slope * t
+ seasonal = seasonal_amplitude * np.sin(2 * np.pi * t / seasonal_period)
+
+ # Generate first series
+ series1_base = trend + seasonal
+ noise1 = np.random.normal(0, noise_std, size=(batch_size, context_length))
+ series1 = series1_base[np.newaxis, :] + noise1
+
+ # Generate second series correlated with first
+ # Create independent pattern for mixing
+ independent_seasonal = seasonal_amplitude * np.cos(2 * np.pi * t / (seasonal_period * 1.5))
+ independent_base = trend * 0.8 + independent_seasonal
+ noise2 = np.random.normal(0, noise_std, size=(batch_size, context_length))
+
+ # Mix correlated and independent components
+ series2 = correlation * series1 + (1 - correlation) * (independent_base[np.newaxis, :] + noise2)
+
+ # Stack into (batch_size, context_length, 2)
+ multi_series = np.stack([series1, series2], axis=-1)
+
+ return multi_series
+
+
+def generate_heavy_payload(
+ batch_size: int = 100,
+ context_length: int = 2048,
+ num_features: int = 1,
+ pattern_type: str = "mixed",
+ noise_std: float = 0.4,
+ seed: int | None = None,
+) -> np.ndarray:
+ """
+ Generate large batch of time series for stress testing and performance evaluation.
+
+ Pattern options:
+ - "mixed": Combination of trends, seasonality, and cyclic patterns
+ - "seasonal": Pure seasonal patterns with varying periods
+ - "trend": Linear trends with varying slopes
+
+ Args:
+ batch_size: Number of independent time series (default 100 for heavy load)
+ context_length: Length of each time series (default 2048 for long sequences)
+ num_features: Number of features per time step
+ pattern_type: Type of pattern to generate ("mixed", "seasonal", or "trend")
+ noise_std: Standard deviation of Gaussian noise
+ seed: Random seed for reproducibility
+
+ Returns:
+ np.ndarray: Shape (batch_size, context_length, num_features)
+ """
+ if seed is not None:
+ np.random.seed(seed)
+
+ # Generate time indices
+ t = np.arange(context_length)
+
+ # Initialize output array
+ series = np.zeros((batch_size, context_length, num_features))
+
+ for i in range(batch_size):
+ for f in range(num_features):
+ if pattern_type == "mixed":
+ # Combine trend, multiple seasonal components, and noise
+ trend = (0.01 + 0.02 * np.random.rand()) * t
+
+ # Primary seasonal component
+ period1 = 100 + np.random.randint(-20, 20)
+ amp1 = 1.0 + 2.0 * np.random.rand()
+ seasonal1 = amp1 * np.sin(2 * np.pi * t / period1 + 2 * np.pi * np.random.rand())
+
+ # Secondary seasonal component
+ period2 = 500 + np.random.randint(-100, 100)
+ amp2 = 0.5 + 1.0 * np.random.rand()
+ seasonal2 = amp2 * np.cos(2 * np.pi * t / period2 + 2 * np.pi * np.random.rand())
+
+ # Combine components
+ base = trend + seasonal1 + seasonal2
+
+ elif pattern_type == "seasonal":
+ # Pure seasonal patterns with varying periods
+ period = 50 + np.random.randint(0, 200)
+ amplitude = 1.0 + 3.0 * np.random.rand()
+ phase = 2 * np.pi * np.random.rand()
+ base = amplitude * np.sin(2 * np.pi * t / period + phase)
+
+ elif pattern_type == "trend":
+ # Linear trends with varying slopes and intercepts
+ slope = -0.05 + 0.1 * np.random.rand()
+ intercept = -5.0 + 10.0 * np.random.rand()
+ base = slope * t + intercept
+
+ else:
+ raise ValueError(f"Unknown pattern_type: {pattern_type}")
+
+ # Add Gaussian noise
+ noise = np.random.normal(0, noise_std, size=context_length)
+
+ # Assign to output array
+ series[i, :, f] = base + noise
+
+ return series
+
+
+def generate_test_suite(seed: int = 42) -> dict[str, np.ndarray]:
+ """
+ Generate a complete test suite with all data patterns.
+
+ Args:
+ seed: Random seed for reproducibility
+
+ Returns:
+ dict: Dictionary containing all generated datasets with descriptive keys
+ """
+ return {
+ # Simple patterns
+ "linear_trend_single": generate_linear_trend_series(batch_size=1, context_length=256, seed=seed),
+ "linear_trend_batch": generate_linear_trend_series(batch_size=16, context_length=256, seed=seed),
+ # Correlated multi-series
+ "correlated_multi_single": generate_correlated_multi_series(batch_size=1, context_length=256, seed=seed),
+ "correlated_multi_batch": generate_correlated_multi_series(batch_size=16, context_length=256, seed=seed),
+ # Heavy payloads
+ "heavy_mixed": generate_heavy_payload(batch_size=100, context_length=2048, pattern_type="mixed", seed=seed),
+ "heavy_seasonal": generate_heavy_payload(
+ batch_size=100, context_length=2048, pattern_type="seasonal", seed=seed
+ ),
+ "heavy_trend": generate_heavy_payload(batch_size=100, context_length=2048, pattern_type="trend", seed=seed),
+ }
+
+
+if __name__ == "__main__":
+ # Example usage and validation
+ print("Generating synthetic time series data...\n")
+
+ # Test linear trend series
+ linear_data = generate_linear_trend_series(batch_size=5, context_length=100, seed=42)
+ print(f"Linear trend series shape: {linear_data.shape}")
+ print(" Expected: (5, 100, 1)")
+ print(f" Min: {linear_data.min():.2f}, Max: {linear_data.max():.2f}, Mean: {linear_data.mean():.2f}\n")
+
+ # Test correlated multi-series
+ multi_data = generate_correlated_multi_series(batch_size=3, context_length=100, seed=42)
+ print(f"Correlated multi-series shape: {multi_data.shape}")
+ print(" Expected: (3, 100, 2)")
+ # Calculate correlation between the two series
+ corr = np.corrcoef(multi_data[0, :, 0], multi_data[0, :, 1])[0, 1]
+ print(f" Correlation between series: {corr:.2f}\n")
+
+ # Test heavy payload
+ heavy_data = generate_heavy_payload(batch_size=100, context_length=2048, seed=42)
+ print(f"Heavy payload shape: {heavy_data.shape}")
+ print(" Expected: (100, 2048, 1)")
+ print(f" Memory size: {heavy_data.nbytes / 1024 / 1024:.2f} MB\n")
+
+ # Generate full test suite
+ print("Generating full test suite...")
+ test_suite = generate_test_suite(seed=42)
+ print(f"Generated {len(test_suite)} datasets:")
+ for name, data in test_suite.items():
+ print(f" {name}: {data.shape}")
diff --git a/examples/model_comparison_simple.ipynb b/examples/model_comparison_simple.ipynb
new file mode 100644
index 0000000..d489828
--- /dev/null
+++ b/examples/model_comparison_simple.ipynb
@@ -0,0 +1,395 @@
+{
+ "cells": [
+ {
+ "cell_type": "markdown",
+ "metadata": {},
+ "source": [
+ "# Simple Model Comparison Example\n",
+ "\n",
+ "This notebook demonstrates basic usage of all three models (FlowState, Chronos2, TiRex) provided by FAIM for both point and quantile forecasting."
+ ]
+ },
+ {
+ "cell_type": "code",
+ "execution_count": 1,
+ "metadata": {
+ "ExecuteTime": {
+ "end_time": "2025-11-05T18:01:16.424417Z",
+ "start_time": "2025-11-05T18:01:16.421605Z"
+ }
+ },
+ "outputs": [],
+ "source": [
+ "import os\n",
+ "\n",
+ "# provide FAIM API KEY here\n",
+ "FAIM_API_KEY = os.environ[\"FAIM_API_KEY\"]"
+ ]
+ },
+ {
+ "cell_type": "code",
+ "execution_count": 2,
+ "metadata": {
+ "ExecuteTime": {
+ "end_time": "2025-11-05T18:01:17.160610Z",
+ "start_time": "2025-11-05T18:01:16.575528Z"
+ }
+ },
+ "outputs": [],
+ "source": [
+ "import matplotlib.pyplot as plt\n",
+ "import numpy as np\n",
+ "from generate_data import generate_linear_trend_series\n",
+ "\n",
+ "from faim_sdk import Chronos2ForecastRequest, FlowStateForecastRequest, ForecastClient, TiRexForecastRequest\n",
+ "from faim_sdk.eval import mae, mse"
+ ]
+ },
+ {
+ "cell_type": "markdown",
+ "metadata": {},
+ "source": "### 1. For demonstration purposes, we generate synthetic data that follows a linear trend with added noise."
+ },
+ {
+ "cell_type": "code",
+ "execution_count": 3,
+ "metadata": {
+ "ExecuteTime": {
+ "end_time": "2025-11-05T18:01:17.175867Z",
+ "start_time": "2025-11-05T18:01:17.171578Z"
+ }
+ },
+ "outputs": [
+ {
+ "name": "stdout",
+ "output_type": "stream",
+ "text": [
+ "Train data shape: (1, 500, 1)\n",
+ "Test data shape: (1, 50, 1)\n"
+ ]
+ }
+ ],
+ "source": [
+ "# Generate synthetic data with linear trend and split into train/test\n",
+ "total_size = 550\n",
+ "train_size = 500\n",
+ "horizon = total_size - train_size\n",
+ "full_data = generate_linear_trend_series(\n",
+ " batch_size=1, context_length=total_size, trend_slope=1.0, noise_std=1.0, seed=42\n",
+ ")\n",
+ "\n",
+ "train_data = full_data[:, :train_size, :]\n",
+ "test_data = full_data[:, train_size : train_size + horizon, :]\n",
+ "\n",
+ "print(f\"Train data shape: {train_data.shape}\")\n",
+ "print(f\"Test data shape: {test_data.shape}\")"
+ ]
+ },
+ {
+ "cell_type": "markdown",
+ "metadata": {},
+ "source": [
+ "## 2. Point Forecasts"
+ ]
+ },
+ {
+ "cell_type": "code",
+ "execution_count": 4,
+ "metadata": {
+ "ExecuteTime": {
+ "end_time": "2025-11-05T18:01:20.572932Z",
+ "start_time": "2025-11-05T18:01:17.194514Z"
+ }
+ },
+ "outputs": [
+ {
+ "name": "stdout",
+ "output_type": "stream",
+ "text": [
+ "Point forecasts generated successfully\n"
+ ]
+ }
+ ],
+ "source": [
+ "# Initialize client\n",
+ "client = ForecastClient(api_key=FAIM_API_KEY)\n",
+ "\n",
+ "# Generate point forecasts from all three models\n",
+ "flowstate_request = FlowStateForecastRequest(x=train_data, horizon=horizon, output_type=\"point\")\n",
+ "flowstate_response = client.forecast(flowstate_request)\n",
+ "\n",
+ "chronos2_request = Chronos2ForecastRequest(x=train_data, horizon=horizon, output_type=\"point\")\n",
+ "chronos2_response = client.forecast(chronos2_request)\n",
+ "\n",
+ "tirex_request = TiRexForecastRequest(x=train_data, horizon=horizon, output_type=\"point\")\n",
+ "tirex_response = client.forecast(tirex_request)\n",
+ "\n",
+ "print(\"Point forecasts generated successfully\")"
+ ]
+ },
+ {
+ "cell_type": "markdown",
+ "metadata": {},
+ "source": [
+ "### Evaluate Point Forecasts"
+ ]
+ },
+ {
+ "cell_type": "code",
+ "execution_count": 5,
+ "metadata": {
+ "ExecuteTime": {
+ "end_time": "2025-11-05T18:01:20.602274Z",
+ "start_time": "2025-11-05T18:01:20.599145Z"
+ }
+ },
+ "outputs": [
+ {
+ "name": "stdout",
+ "output_type": "stream",
+ "text": [
+ "Point Forecast Metrics:\n",
+ "--------------------------------------------------\n",
+ "FlowState - MSE: 3.5633, MAE: 1.4007\n",
+ "Chronos2 - MSE: 1.9375, MAE: 1.1466\n",
+ "TiRex - MSE: 6.2015, MAE: 2.0716\n"
+ ]
+ }
+ ],
+ "source": [
+ "# Calculate metrics for each model\n",
+ "models = {\"FlowState\": flowstate_response.point, \"Chronos2\": chronos2_response.point, \"TiRex\": tirex_response.point}\n",
+ "\n",
+ "print(\"Point Forecast Metrics:\")\n",
+ "print(\"-\" * 50)\n",
+ "for name, pred in models.items():\n",
+ " mse_score = mse(test_data, pred)\n",
+ " mae_score = mae(test_data, pred)\n",
+ " print(f\"{name:12s} - MSE: {mse_score:.4f}, MAE: {mae_score:.4f}\")"
+ ]
+ },
+ {
+ "cell_type": "markdown",
+ "metadata": {},
+ "source": [
+ "### Visualize Point Forecasts"
+ ]
+ },
+ {
+ "cell_type": "code",
+ "execution_count": 6,
+ "metadata": {
+ "ExecuteTime": {
+ "end_time": "2025-11-05T18:01:20.728973Z",
+ "start_time": "2025-11-05T18:01:20.626677Z"
+ }
+ },
+ "outputs": [
+ {
+ "data": {
+ "image/png": "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Yd99J02f7tw2JV+eeIJDDqD4hIeHq02bNrYFAABwE0IpAChB2dnSXXflrf/9704wBQDFxePxKDs727YAAABuQigFACXojTekVauc2y1bSkOHcrgBFK+DBw/qq6++si0AAICbEEoBQAk5cEB6+OG89QkTJD8/DjeA4hUWFqYWLVrYFgAAwE0IpQCghIwdK+3f79weOFC64AIONYDiFxQUpBo1atgWAADATQilAKAErF0rvfKKczs0VPrHPzjMAEpGRkaGdu3aZVsAAAA3IZQCgGJmag2PHCnl5DjrZghfzZocZgAlIzU1VatXr7YtAACAmxBKAUAxmzVLWrjQuV2vnnTPPRxiACUnJiZGF198sW0BAADchFAKAIpRWpp077156y++KAUHc4gBlBwfHx/5+/vbFgAAwE0IpQCgGD3zjLRjh3P7kkukvn05vABKVkpKilauXGlbAAAANyGUAoBisnWrE0oZ/v7SSy+ZHgwcXgAly+PxKCsry7YAAABuQigFAMUgOVm67TYzC5azfvfdUuPGHFoAJS8iIkLnnnuubQEAANyEUAoAztBPP0nt20sLFjjr1apJf/0rhxUAAAAAToVQCgDOwAcfOIGUCaaMyEhp+nSnBYDScODAAX3xxRe2BQAAcBN/b+8AAHhDYqL06qtO7achQ6TY2NN7flaW9MgjeTWkjGbNnJCqQYNi310AOKnQ0FA1adLEtgAAAG5CTykAFYoJk0wYFR8vPfSQ9Je/SLVqSXfcIW3aVLjX2LtX6tWrYCA1cKC0dCmBFIDSFxwcrNq1a9sWAADATQilAFQY8+ZJrVtL//d/0v79edvT06XXXnMKk/frJ333nZnN6sSvsWyZ1LatNH++s+7n58yy9+67UlhY6bwPAMjvyJEj+uOPP2wLAADgJoRSAMq9X36RLr9cuvhiad26vO3XXSeNGiWFhzvrJoj6+GPpgguk886T3n/f6VmV6403pM6dpR078gqam+LmZqY9H59SflMA8D8pKSlasWKFbQEAANyEUApAua4bde+9UtOm0n/+k7e9XTtp0SLp3/+WXnzRCZnMULzq1fMe88MPUv/+znC88eOl22+Xhg41PRKc+zt2lFaskLp0Kf33BQD5RUdH66KLLrItAACAmxBKASh3TO8mMxzPBEovvCBlZjrbzzpLmjpV+v57qVOnvMebv+NMbamtW6Vp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Ur78CwIAwKvCwsLUvHlz2wIAALgJkUIF8sILztC73J5ORmKi9NJLUuPG0sUXO/WizAx5+e3eLV1yiZSQ4Kyff7703ntSQEDp7j8AADheUFCQatasaVsAAAA3IZSqAEz9J9Mb6t5787YNGuQUJw8Ozts2b5509dVS3brSE084YZQJrXr3lrZscR7TrJn0n/9IoaGl/z4AAMDxjhw5ot27d9sWAADATQilyrn0dOm665zeULkee0yaOlWaMkXauVN67jkpPj7v/t9/l8aOlWrXllq1klavdrbXqSN98YUUE1P67wMAAJxYSkqKVq1aZVsAAAA3IZQqx0y9UzPs7v33nXU/P6eWlAmcfHycbZUrOz2oTE2pL790Cpnn1okyw/h++825HRsrffWVU4MKAACUHTExMerRo4dtAQAA3IRQqpzavt2p/fTtt866GW73ySfSrbee+PEmiOrZU/roI2nrVunhh6WqVZ37IiKkOXOkhg1Lb/8BAEDh+Pj4KCAgwLYAAABuQihVDpnhdh07Shs3OusmXPrmG6lPn8I93wzbe/JJaccO6euvnddp165EdxkAABQRw/cAAIBb+Xt7B1C85s+XrrxSSk521hs0cHo51a9/+q8VGCh168ZvCACAsszj8dgi56YFAABwE3pKlSPvvuvMlJcbSHXoIC1aVLRACgAAuENERITat29vWwAAADchlCon3nlHuvFGKTPTWb/8cmnBAqlKFW/vGQAAAAAAwPEIpcoJM8tefLxze9gwafZsp7g5AAAo3w4ePKgvv/zStgAAAG5CTalywvSI+uIL6eOPpdGjzUw83t4jAABQGkJCQtS4cWPbAgAAuAmhVDliakfdc4+39wIAAJSm4OBg1alTx7YAAABuwvA9AAAAFzMz7yUkJNgWAADATQilAAAAXCwlJUU//vijbQEAANyEUAoAAMDFoqOjdeGFF9oWAADATQilAAAAXMzX19fWkzItAACAm3D1AgAA4GKpqalau3atbQEAANyEUAoAAMDFsrOzbT0p0wIAALgJoRQAAICLRUZGqmPHjrYFAABwE0IpAAAAAAAAlDpCKQAAABc7ePCg5s2bZ1sAAAA3IZQCAABwsZCQENWrV8+2AAAAbkIoBQAA4GLBwcE2lDItAACAmxBKAQAAuFhmZqb2799vWwAAADchlAIAAHCx5ORk/fDDD7YFAABw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",
+ "text/plain": [
+ ""
+ ]
+ },
+ "metadata": {},
+ "output_type": "display_data"
+ }
+ ],
+ "source": [
+ "# Plot all forecasts together\n",
+ "fig, ax = plt.subplots(figsize=(12, 6))\n",
+ "\n",
+ "# Plot only last 'horizon' points of training data\n",
+ "train_context_start = max(0, train_size - horizon)\n",
+ "train_context_indices = np.arange(train_context_start, train_size)\n",
+ "ax.plot(train_context_indices, train_data[0, train_context_start:, 0], label=\"Training Data\", color=\"blue\", linewidth=2)\n",
+ "\n",
+ "# Plot test data\n",
+ "test_indices = np.arange(train_size, train_size + horizon)\n",
+ "ax.plot(test_indices, test_data[0, :, 0], label=\"Test Data\", color=\"black\", linewidth=2, linestyle=\"--\")\n",
+ "\n",
+ "# Plot forecasts\n",
+ "colors = {\"FlowState\": \"green\", \"Chronos2\": \"red\", \"TiRex\": \"orange\"}\n",
+ "for name, pred in models.items():\n",
+ " ax.plot(test_indices, pred[0, :, 0], label=f\"{name} Forecast\", color=colors[name], linewidth=1.5, alpha=0.8)\n",
+ "\n",
+ "ax.axvline(x=train_size, color=\"gray\", linestyle=\":\", linewidth=1, alpha=0.7)\n",
+ "ax.set_xlabel(\"Time\")\n",
+ "ax.set_ylabel(\"Value\")\n",
+ "ax.set_title(\"Point Forecast Comparison\")\n",
+ "ax.legend(loc=\"best\")\n",
+ "ax.grid(True, alpha=0.3)\n",
+ "plt.tight_layout()\n",
+ "plt.show()"
+ ]
+ },
+ {
+ "cell_type": "markdown",
+ "metadata": {},
+ "source": "## 3. Quantile Forecasts. Chronos2 supports custom quantile levels, while TiRex and FlowState can only return the default ones."
+ },
+ {
+ "cell_type": "code",
+ "execution_count": 7,
+ "metadata": {
+ "ExecuteTime": {
+ "end_time": "2025-11-05T18:01:23.556084Z",
+ "start_time": "2025-11-05T18:01:20.748416Z"
+ }
+ },
+ "outputs": [
+ {
+ "name": "stdout",
+ "output_type": "stream",
+ "text": [
+ "Quantile forecasts generated successfully\n"
+ ]
+ }
+ ],
+ "source": [
+ "# Generate quantile forecasts from all three models\n",
+ "quantile_levels = [0.1, 0.2, 0.3, 0.4, 0.5, 0.6, 0.7, 0.8, 0.9]\n",
+ "\n",
+ "flowstate_q_request = FlowStateForecastRequest(\n",
+ " x=train_data, horizon=horizon, prediction_type=\"quantile\", output_type=\"quantiles\"\n",
+ ")\n",
+ "flowstate_q_response = client.forecast(flowstate_q_request)\n",
+ "\n",
+ "chronos2_q_request = Chronos2ForecastRequest(\n",
+ " x=train_data, horizon=horizon, output_type=\"quantiles\", quantiles=quantile_levels\n",
+ ")\n",
+ "chronos2_q_response = client.forecast(chronos2_q_request)\n",
+ "\n",
+ "tirex_q_request = TiRexForecastRequest(x=train_data, horizon=horizon, output_type=\"quantiles\")\n",
+ "tirex_q_response = client.forecast(tirex_q_request)\n",
+ "\n",
+ "# Create dictionary of quantile models for visualization\n",
+ "quantile_models = {\n",
+ " \"FlowState\": flowstate_q_response.quantiles,\n",
+ " \"Chronos2\": chronos2_q_response.quantiles,\n",
+ " \"TiRex\": tirex_q_response.quantiles,\n",
+ "}\n",
+ "\n",
+ "print(\"Quantile forecasts generated successfully\")"
+ ]
+ },
+ {
+ "cell_type": "markdown",
+ "metadata": {},
+ "source": [
+ "### Visualize Quantile Forecasts"
+ ]
+ },
+ {
+ "cell_type": "code",
+ "execution_count": 8,
+ "metadata": {
+ "ExecuteTime": {
+ "end_time": "2025-11-05T18:01:23.853901Z",
+ "start_time": "2025-11-05T18:01:23.589279Z"
+ }
+ },
+ "outputs": [
+ {
+ "data": {
+ "image/png": "iVBORw0KGgoAAAANSUhEUgAABKUAAASmCAYAAAD/KRjlAAAAOnRFWHRTb2Z0d2FyZQBNYXRwbG90bGliIHZlcnNpb24zLjEwLjcsIGh0dHBzOi8vbWF0cGxvdGxpYi5vcmcvTLEjVAAAAAlwSFlzAAAPYQAAD2EBqD+naQABAABJREFUeJzs3QV8U/f6BvCn7t7SFkqL23AdNgaMIRvbmN0pc7vz/eduzN3dlTlzxthgG+7D3V3r3v/neXMT0tJCKbW0z3f33NMkJycnIW2SJ+/v/XkVFRUVQUREREREREREpBp5V+eNiYiIiIiIiIiIkEIpERERERERERGpdgqlRERERERERESk2imUEhERERERERGRaqdQSkREREREREREqp1CKRERERERERERqXYKpUREREREREREpNoplBIRERERERERkWqnUEpERERERERERKqdQikRERFxadKkCS688EI9IvXUsccea4vT2rVr4eXlhffee69Gj0tERETqJoVSIiIi9QBDBYYLpS233357jR7b33//jeHDh6NRo0YIDAxEcnIyRo4ciU8++cS1TWZmJu6//378+eefFb6dKVOm2D727t2LqrBo0SKcd955dj8CAgLQsGFDO7148WLUJjwePg4MnGpCWc/DhIQE1HU1/diLiIjUNr41fQAiIiJSfR588EE0bdq02Hnt27evsX+CL774Av/5z3/QuXNnXH/99YiKisKaNWswefJkvPnmmzjnnHNcodQDDzxgP7tX8hxuKMV9sBIsMjKyUu/H119/jbPPPhvR0dG45JJL7DFm8PD222/jyy+/xOeff46TTz4ZtSUY4ePAx5GVce7Gjx9fLccwZMgQjB49uth5QUFBqOsO9tiLiIjURwqlRERE6hFWJHXv3h21BatG2rVrh2nTpsHf37/YZdu3b4cnWLVqFc4//3w0a9bMwrS4uDjXZQza+vfvbxVTCxYsOCAQrG1K/htUlVatWtljUtny8/NRWFhYbfdDREREjoyG74mIiMhBrV69GmeccYZVAQUHB+Poo4/Gjz/+6Lq8qKgIsbGxuOmmm1znMRhgNZKPj0+x4XKPP/44fH19kZ6e7gp0evToUWqI0KBBA1uz4sgZ9LDKxDnci4EWMexh9RNDIQ7/4zCwiy++GLt27XLti9vecsst9jODIec+3IdRffTRR+jWrZtV7PC+nnXWWdiwYcMhnx1PPvmkVXK98cYbxQIp4uPy+uuv2/3ldk483tIqZXicPC537777LgYNGmSPB4cFMsR79dVXD7gu93fiiSfacMiePXvaY8HH5IMPPig2jJP/ljRw4EDX4+AcFlmyp1RZli5ditNPP90eJ94Og85x48ahsjCQZMVZfHy87b9Tp054//33i23j7Hf11FNP4bnnnkPz5s3t8XEOlyzvMfL5eeONN9rjx+snJSVZFdfOnTvt8tzcXNx777323IiIiEBISIgFjX/88ccB+/rss89su7CwMISHh6NDhw54/vnny/XYi4iI1EeqlBIREalH9u3b5/qw7R6clGXbtm3o06ePhS7XXXcdYmJiLBw46aSTbFjaqFGj7IN13759rUrIiUERb8vb2xv//PMPTjjhBDv/r7/+QpcuXRAaGmqnU1JS8Pvvv2Pjxo0WBpSGQQ9DmKuuuspu79RTT7XzO3bsaOvffvvNgrOLLrrIAin2dmJAxDUrsHh8vM7y5cvx6aef4tlnn3XdZ2eINGbMGNxzzz0488wzcemll2LHjh148cUXccwxx2Du3LkHHe73/fffW6DBoKI03Acv53avvPIKDhfv+1FHHWWPOQM97ue///2vBX9XX311sW1XrlxpQQwDnQsuuADvvPOOBWAMSrgPHgv/HV944QXceeedaNu2rV3PuS4PPq7892bvLPYjY0gzduxYnHLKKfjqq6/s3+hQsrOzD3geMshhKJSVlWXBGO/LNddcYyEih3nyfjBAYvVZydCO+7v88svt+gyhynuMDAv577ZkyRILMrt27WrHxfCKz0k+T1JTU/HWW2/Z8MzLLrsMaWlpNixz6NChmDFjhg09dT4Puc3gwYMtfCXul89/HnNlPPYiIiJ1TpGIiIjUee+++24RX/ZLW9ylpKQUXXDBBa7TN9xwg23z119/uc5LS0sratq0aVGTJk2KCgoK7Lwnn3yyyMfHpyg1NdVOv/DCC7avnj17Ft122212HreNjIwsuvHGG137evvtt23//v7+RQMHDiy655577Lac+3XasWOHbXffffcdcN8yMzMPOO/TTz+17SdPnuw6j8fI89asWVNs27Vr19qxjxkzptj5//77b5Gvr+8B57vbu3ev7fPkk08uOpiTTjrJtnM+PnyM+fiUxPtX8t+ktPs3dOjQombNmhU7j/sreZ+3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/8iKVGIoRXFxcRZMcT9+fn56fEVExGRkZODff/9FkyZNFEqJiIiIR1Gj80MoKCiwtYZMSU1zPgedz0kRERGKiorC0KFDbS0iIiLiSRRKlZOGS0lN03NQRETKen1gewG9ToiIiIinUSglIiIi4sE4icbs2bNtLSIiIuJJFEpJubFXxXPPPVfu7f/880/71nbv3r16lEVERERERESkGIVSdRCDoIMt999/f4X2O3PmTFx++eXl3r5Pnz42WxxnA6pKzvDLOXyBt9elSxfceuutdvuHi/v59ttvq+RYRUREKltYWBi6detmaxEREfFc2fnZ2Jq+FVl5WagvNPteHeQexHz++ee49957sWzZMtd5oaGhxWaFY+NszgpXntnfDrcxd0JCAqoL72N4eDhSU1MxZ84cPPHEE3j77bcttOrQoUO1HYeIiEh14mt5YWGhrUVERMTzgqg9WXuwPWO7LTzdJbELkvySUB+oUqoOYhDkXFg1xMof5+mlS5faN6k///yzfasaEBCAv//+G6tWrcLJJ5+M+Ph4C6169OiBCRMmHHT4Hvf71ltvYdSoUQgODkbLli0xbty4Mofvvffee4iMjMSvv/6Ktm3b2u0MGzasWIiWn5+P6667zraLiYnBbbfdhgsuuACnnHLKIe93gwYN7D62atUKZ511Fv755x8L0q666qpi1V5DhgxBbGysPTYDBgywAMv9PhLvE4/debo8j4+IiEhN2LNnj722ci0iIiK1H4OnLWlbMH/rfPy17i9M2zgN6/aug5+3HwoK69ds6wql6qnbb78djz32GJYsWYKOHTsiPT0dI0aMwO+//465c+daWDRy5EisX7/+oPt54IEHcOaZZ2LBggV2/XPPPRe7d+8uc/vMzEw89dRT+PDDDzF58mTb/8033+y6/PHHH8fHH3+Md99910IlVj1VdChdUFAQrrzyStvP9u3b7Tw2gWXIxSBu2rRpFqTxuJ3NYRlaEW+fYZnzdEUfHxERkaoWEhJiFcFci4iISO2Uk59jQ/MWbFuAv9f9jembpruCqKTwJDQKb4SwgLB6N5uuhu9VQPfuwNatqHYcCTdrVuXs68EHH7SKIafo6Gh06tTJdfqhhx7CN998Y5VP11xzTZn7ufDCC3H22Wfbz4888gheeOEFzJgxw0Kb0uTl5eG1115D8+bN7TT3zWNxevHFF3HHHXdY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YabqWyai3xepf/LzgV27OJudI2hyBk/O8IlLWZPU7NgBxMYebN/52LBhg03a4utbq9qFitRKel8mot8XqT05y2G/c4mKinJN7deoUSMsXLjQQik2JM/MzDyyoxYRERHxQEuXApMmOUKn0hYGUof3NeB+a9cePJTiezB+SciWCLEH21BERESklil3KMXwqX379jjmmGOsmTiDqDPOOAPXX389Jk6caOcNHjy4ao9WREREpBZZvBh44AFg7NiK74PFTU2aAM2aAc2bOxbnz02bAmFhB78+2yl0797d1iIiIiJ1MpTq2LEjevTogVNOOcXCKLrrrrtshrwpU6ZY3yc2GRcRERGpD5VRbGH52WcHr4AKCgLi4wFOEuxceJohlDOASkpyBFMVxfdi7MfJtYiIiIgnKfdboEmTJuHdd9/Fo48+ijFjxlgIdemll+L222+v2iMUERERqSVWrHCEUZ98wr40+89n2HTDDUD79sUDqJCQqj+m7OxsrF271vo1BAcHV/0NioiIiFR3KNW/f39bXnzxRYwdOxbvvfceBgwYgBYtWuCSSy6xWe8SEhIq67hEREREDsv48cBzz7HHkqN6qbSFQZKzsolvW9q0Adq23b9mS6bSZrpjQ/KHHgI++ggoKNh/Pre/9Vbgv/+tngCqNFlZWVi+fDlatmypUEpERETq9ux77lauXGnVUx9++CG2bt2KYcOGYdy4cfA0mn1PqppmeRHR74tUrXfeAS67rHj1UkVERxcPqlq2BL7/HnjvveJhFLe75RbgmmuA0FDUKL3GiOh3RkSvMVJvZt9zxyqpO++8EykpKbjjjjvw448/HsnuRERERA7b008DN99c9uWsfHIu3t6ONb+Sy8s7cNvdu4EpUxxLaaKigP/7P+Daa4GDvL8SERERkXKocCg1efJkvPPOO/jqq6/g7e2NM88804bxiYiIiFQHBkt33QU8+uj+866/HnjqKcDHp/RheO7X3boVWLLE0bSci/PnjRsP3D4iArjpJsf++XNt+yZy+vTp1lYhMjKypg9HREREpGpCqc2bN1svKS4cutenTx+88MILFkiF1FQjBREREal3OJTu6quB11/ffx4bkHMi4IOFUU7cJjHRsQwaVPyytDRg2TJHQMU1+0ZdcAFQW/MefjkYGBhoaxEREZE6GUoNHz4cEyZMQGxsLEaPHo2LL74YrVu3rtqjExERESkhNxc4/3xg7Nj95730kiOkqgxhYUD37o7FE4SGhqJTp062FhEREamToZSfnx++/PJLnHjiifBhTbyIiIhINcvIAE47Dfj1V8dpX1/g/feBc86pv/8UbHSem5tra1VLiYiISJ0MpTxxVj0RERGpO/bsAU44AZg61XE6MBD46itgxAjUa3v37sXvv/+OkSNHWkW7iIiIiKc4otn3REREREravNkRICUlOWaoK0+Pp0PZsgUYOhT491/HaTYb/+EHoF8/Pf4cttelSxcN3xMRERGPo1BKREREKkVqqmOGurff3n8e2xwxnOLSuPH+n50Lm4ezaXleHpCfv39xP80he9deC6xe7dhngwaO4XudO+sfjvz9/ZGQkGBrEREREU+iUEpERESO2B9/ABddBKxbV/z89HTHLHZcKkNKCvDbb0DLlpWzv7ogJycHGzZsQEREBIKCgmr6cERERETKTXMHi4iISIVlZgLXXw8MGrQ/kGJ1FBuPDx4McKLe4ODKeYDbtQP++UeBVEkZGRlYuHChrUVEREQ8iSqlREREpEKmTQMuuABYvnz/eQMGAO++CzRtuv+8oiI24wY2biy+bNgApKVxhl/Hwpn0uLj/7DydmAice66jl5QUFx0djeHDh9taRERExJMolBIREZHDkpMDPPgg8NhjQGHh/pnweJq9n7xL1GGz0XlUlGPp0EEPtoiIiIg4aPieiIiIlNv8+UDPnsAjj+wPpHh67lzHML6SgZRUvbS0NMyaNcvWIiIiIp5ElVIiIiJiONMdq6Cysw9ceP7EiY4KKc6MRxxWd999wG23OYbZSc3w8vKCt7e3rUVEREQ8id5CioiI1EHLlgHjxwOpqY6FRTSlrblwhjwGTwUF5d8/h+F98AHQuXNV3gspj9DQUHTt2tXWIiIiIp5EoZSIiEgd88UXwFln7R9eV5k4PI+VUayQCgio/P3L4SsqKkJBQYGtRURERDyJQikREZE65LvvgHPOOXQg5eMDhIcDYWGOJSjIETKxYTkX95+dp1mIc8opQNeu1XVvpDz27NmD8ePHY+TIkYiNjdWDJiIiIh5DoZSIiEgd8fPPwBlnOHpD0bnnAmee6QifnAGU82cGTWpBVDeEhISgY8eOthYRERHxJAqlRERE6oDffwdOPXV/E3IGUu+/76iIkrotICAAjRo1srWIiIiIJ9HE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RER8GGdJ1a1bVzOlRERExOcolBIRERERERERkTiXPO5vMuFxOBzm8PLly/G9K5JIhYSEmKW6AwIC4O+vLFhEzxeRmMPWvfnz56NJkybInDmzHloRvS4TiTF6HyPRZecrdt4SHoVSgAkLKF++fNF+wEVERERERERExDVv4aiB8Pg5Ioutkkj6e/z4cQQGBsLPzy++d0cSaUrM0PPIkSNInz59fO+OSIKm54uInjMi+jsjkjDodZlEF6MmBlK5c+eOsFtIlVIcrOXvj7x580b7wRbxFgMphVIier6IxAb9jRHRc0YktuhvjERHRBVSNg23ERERERERERGROKdQSkRERERERERE4pxCKZE4kCpVKgwfPtwcioieLyL6GyMSf/S6TETPF0k4NOhcRERERERERETinCqlREREREREREQkzimUEhERERERERGROKdQSkRERERERERE4pxCKZEY8O6778LPzw8DBw50OX7lypVo3Lgx0qZNi/Tp06N+/fq4du1a6Onnz5/Ho48+ak7LmDEjnnzySQQFBelnIknyOXPy5El0794dOXPmNM+ZypUr4/fff3e5nJ4zklS88cYb5jnivJUsWTL09OvXr6Nfv37IkiUL0qVLh86dO+PUqVMu13H48GG0adMGadKkQfbs2TF48GDcvn07Hu6NSPw+Z/i3Y8CAAShRogRSp06N/Pnz47nnnsOlS5dcrkPPGUkqIvsbY3M4HGjVqpU5/c8//3Q5Tc8XiSnJY+yaRJKotWvXYvz48Shfvvw9gVTLli0xdOhQjB07FsmTJ8fmzZvh7x+WBTOQOnHiBObOnYtbt26hV69e6NOnD3766ad4uCci8fuc6dGjBy5evIjp06cja9as5nnQtWtXrFu3DpUqVTLn0XNGkpIyZcpg3rx5od/z74jthRdewL///ovffvsNGTJkQP/+/dGpUycsX77cnH7nzh0TSDHkXbFihflbw+dYihQpMGrUqHi5PyLx9Zw5fvy42caMGYPSpUvj0KFDeOaZZ8xxU6dONefRc0aSmoj+xtg+/vhjE0i50/NFYpRDRKLtypUrjmLFijnmzp3raNCggeP5558PPa1GjRqOYcOGhXvZ//77z8Gn4Nq1a0OPmzlzpsPPz89x7Ngx/VQkyT1n0qZN65g8ebLL+TNnzuyYMGGC+VrPGUlKhg8f7qhQoYLH0y5evOhI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3fF9d0REREQMzZQSERERSeDKly8f+nWyZMmQJUsWlCtXLvQ4tufR6dOnzeHmzZtNAOVpPtW+fftQvHjxONlvERERkYgolBIRERFJ4FKkSOHyPWdROR9nr+oXEhJiDoOCgtCuXTuMHj36nuvKlStXrO+viIiIiDcUSomIiIgkMpUrV8bvv/+OggULInlyvdwTERGRhEkzpUREREQSmX79+uH8+fPo1q0b1q5da1r2Zs+ebVbru3PnTnzvnoiIiIihUEpEREQkkcmdOzeWL19uAiiuzMf5UwMHDkTGjBnh76+XfyIiIpIw+DkcDkd874SIiIiIiIiIiCQt+qhMRERERERERETinEIpERERERERERGJcwqlREREREREREQkzimUEhERERERERGROKdQSkRERERERERE4pxCKRERERERERERiXMKpUREREREREREJM4plBIRERERERERkTinUEpEREREREREROKcQikREREREREREYlzCqVERERERERERCTOKZQSEREREREREZE4p1BKRERERERERETinEIpERERERERERGJcwqlREREREREREQkzimUEhERERERERGROKdQSkRERERERERE4pxCKREREfHIz88P/fv316OTBBQsWBCPP/546PeLFi0yP38eioiIiMQWhVIiIiJJzL59+/D000+jcOHCCAgIQPr06VGnTh188sknuHbtGpKKtWvXmtCtTJkySJs2LfLnz4+uXbti9+7dUbqe5cuXo2PHjsiRIwdSpUplAp5nnnkGR44cQUKyYsUKvPHGG7h48WKc3/bBgwdNyOVpq1mzJhK7+HzsRUREErLk8b0DIiIiEnf+/fdfPPjggyY86dGjB8qWLYubN29i2bJlGDx4MLZv346vvvoqSfxIRo8ebQIlPh7ly5fHyZMnMW7cOFSuXBmrVq0yj01kxo4di+eff94EfAMGDECuXLmwY8cOfP3115gyZQpmzpyZYEIXBiMjRowwFVEZM2Z0OW3Xrl3w94/9zyq7deuG1q1buxyXLVs2JHYRPfYiIiJJmUIpERGRJOLAgQN4+OGHUaBAASxYsMAEKLZ+/fph7969JrSKCVevXjXVRwnZoEGD8NNPPyFlypShxz300EMoV64c3n33Xfzwww8RXp6B1sCBA1G3bl3MmjULadKkCT2tb9++pvqsc+fOJuhL6EEEQ8q4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HX3xxRcduXLlcqROndo8R1auXHnP76Q3v2sRPfYiIiJJmR//ie9gTERERCSx4FBsDrlmVQxXzxMRERERz9S+JyIiIhKDuGLb8ePHzaDr/Pnzm/YwEREREbmXKqVERERERERERCTOafU9ERERERERERGJcwqlREREREREREQkzimUEhERERERERGROKdQSkRERERERERE4pxW3wMQEhJiVskJDAyEn59f3P8UREREREREREQSCYfDgStXriB37tzw9w+/HkqhFGACqXz58sXlz0dEREREREREJFE7cuQI8ubNG+7pCqUAUyFlP1jp06ePu5+OJKlqvDNnziBbtmwRpsQioueLSFRdunQJy5cvR506dZAhQwY9gCJ6XSYSY/Q+RqLr8uXLpvjHzlvCo1AKCG3ZYyClUEpi6z/z69evm98vhVIier6IxLSMGTOaQEqvY0T0ukwkJul9jNyvyEYkqWRDRERExIelS5cOFStWNIciIiIivkShlIiIiIiPDxK9deuWORQRERHxJQqlRERERHzYhQsXMG/ePHMoIiIi4ks0UyoKvbQ3b96M3Z+GJOrfH36KzblS0Z0plTJlSs2jEhGRe6h9T0RERHyVQikvMIw6cOCACRZEooMtFfz9uXLlSqSD3sLDMKtQoUImnBIREbHx70KuXLn090FERER8jkIpL8KEEydOIFmyZGY5Q62cJtH9Pbp9+zaSJ08erVCKgdbx48fN72L+/PmjHWyJiEjic+PGDRw9etSsvpc6der43h0RERERrymUigSDhODgYOTOnRtp0qTx/pEVicFQirJly2aCKV5PihQp9PiKiIhx9epVbN26FQULFlQoJSIiIj5Fg84jcefOHXOolimJb/bvoP07KSIiQpkyZUKLFi3MoYiIiIgvUSjlJbVLSXzT76CIiIT394HjBfR3QkRERHyNQikRERERH8ZFNNavX28ORURERHyJZkqJ1zirYuDAgWbzxqJFi9CoUSNcuHABGTNm1CMtIiIiIiIiSdJHH32EefPmha7M7rw5nI5744030KRJEyQVCqUSocjK94cPH25+0aNq7dq1SJs2rdfnr127tlktjqsBxSY7/LLve2BgIAoXLoxmzZrhhRdeMMtkRwWvY9q0aXjggQdiaY9FRERiDv/uValSxRyKiIhIwrRlyxbMmDEj0vOdPnmSK2XxjSmSArXv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1q9fj6uuusrMgoqMjKw0VbvkkkvMnCgaMmQIZs2ahZdfftkESzXFGVXXXHONT6VUx44d0bJlSzMwXaS6/v6bv6tBKC62Q2avu86FSZPiAMR5fp85gJa/YwqlRKqm14tI9V8zHHSu9xgR/18z+lwmoteL1K2Kcp5GFUr98ssvpkVv6NChnuP47d7s2bPx9NNPY9myZea4fv36+Vyub9++WLdunfm5TZs25jq8FRUVmTJ2nlaZiIgIs5XFsECBgVTX9u3Asccy3LSHuXLeAw8EITjYdxUkfvjR75iIf/R6EalepdTGjRvNSIMoDR4U0fuMSC3T5zKpCX+zlQbrI2Ib3p9//onffvvNs3E2FGdI8edu3bqZQeVOOOVYvnw5OnfubH4+4IADkJaWZgIux7fffmu+/Rg2bFi9PyZpftjtefzxwOrV9vCQIcCbbwIhIQ19z0REpLnIzs42n6m4FxEREQkkDVYpFRcXhwEDBvgcFxMTYwZ0Osdff/31mDJlCvbaay8zU+q1117D0qVL8f7773uqpo488khcdNFFeP75583KMxMnTsRpp52mlfekznGRowsvBObNs4e52ONnn9mh5SIiIvUlKSkJRxxxhNmLiIiIBJIGX32vKpMmTTKDx6+++mrTksdwijOoOMzT8eabb5ogipVXLA8bP348nnzyyQa939J0cF4sO0Q3bfLdNm9m1R7www/2fNHRNpBq376h77GIiDTXtgruRURERAJJkIvLgTVzHHSekJBgVkfToPPm7c8/geefB37+2YZPW7dyGGbVl+HfAB98AJxwQuXnYUsp559xtUjNLROpml4vItXDzy9z5szBQQcdZD7PiIjeZ0Rqiz6XSV3nLI26UkqkPhQWAh99BDzzDDB7dvUuy8/+Dz9cdSAlIiIiIiIiIuUplJJmiy14L74ITJ1qq6K8caEALuDIOVHc2rYt/dl7a9HCnldERKQh53TuvffeZi8iIiISSBRKSbPCZtW5c21VFOflFxX5nt6nDzBxInD22UAVFYYiIiKNBicxsL1CExlEREQk0CiUkoDGeU+//Qakpto2vIICu3c278NZWcC77wK//+57Hax0Ou444PLLgVGj7IwoERGRQLFr1y5Mnz4d48aNQwuW8IqIiIgECIVSEpBY4fTee8D999vh5DXBz+0XXwxccgnQqVNt30MREZH6ERMTg4EDB5q9iIiISCBRKCUBJT8feP114IEHgJUra3Ydw4bZqqiTTwYiI2v7HoqIiNSviIgIdOjQwexFREREAolCKQkI2dnASy/Zle42biwfMh12GBAeDoSF2c37Z++NM6MGDWqoRyEiIlL7CgoKsHnzZiQmJiJS37aIiIhIAFEoJY1aWhrw7LPAY48BO3b4nsYg6uabgUMP1RwoERFpvrKysvDbb7+hY8eOCqVEREQkoCiUkkY5L2rJEuCdd+wqeRkZvqcfeywweTKw//4NdQ9FREQaj6SkJIwePdrsRURERAKJQilpUC4XsGoVsHAhsGCB3X79FcjNLb9C3mmnATfdBAwc2FD3VkREpPEJCgpCWFiY2YuIiIgEEoVSUiuWLwf+/hsICbEbQyTvvffP27eXhlDcp6ZWfr2cA3XuucANNwA9eugfS0REpLL2veHDhyM+Pl5PkIiIiAQMhVKyR/LybPXSE0/U3hPZtSuw777AfvsBp54KdOhQe9ctIiLS1LhcLjPsnHsRERGRQKJQSmrsjz+AM88E/vqr5tfRsqUNn5wQap997HEiIiLin7i4OOy3335mLyIiIhJIFEpJtZWUAI8/boeNFxTY4yIigCuuABITgeJie57K9lyteuhQG0R17qyV80RERERERESaI4VSUi0bNwITJgCzZpUeN2gQ8NZbQP/+ejJFRETq265duzB9+nQcc8wxSElJ0T+AiIiIBIzghr4DUr8KC4EnnwTGjgUuugh45x07eNwf779vV77zDqSuu84OLFcgJSIi0jCioqLQp08fsxcREREJJKqUaiY4+/TLL4FrrwWWLSs9/qWX7H7IEGD0aLsdfDA/4JaeJzMTuPJK4NVXS49r3x6YNg0YNaoeH4SIiIiUExkZic6dO5u9iIiISCBRKNUM/P03cM01wPTplZ9n8WK7PfywnQ914IE2oOrZE7jxRmDVqtLznnwy8PzzQHJyvdx9ERERqQJX3tu2bRsSExMVTImIiEhAUSjVhO3cCdxxB/Dcc3bIuIOB00MPATk5wIwZwMyZNpByVpLOzwe+/dZu3mJjgaefBs45R8PJRUREGousrCz88ssvaNeunUIpERERCSgKpZro3CgGUQykdu0qPb5TJ1sJxUqnoCB7HKuhaMcOG0IxoGJQtWaN73UOHw68/jrQrVs9PhARERHZLVZIHXrooWYvIiIiEkgUSjUxX39tW/X++af0uOhoYPJkO0+qshmoLVoAp5xiN1ZMsV2PAdXcucDgwXamVKh+W0RERBqd4OBgUyHFvYiIiEggUczQRCxfDkyaBHz1le/xbLW7/36gXTv/r4tVVN272+2SS2r9roqIiEgtys7Oxp9//on9998fcXFxem5FREQkYCiUaiI2bPANpNhu9/jjwL77NuS9EhERkbpWXFxs5kpxLyIiIhJIVOfdRIwaBRx3HNCxI/DWW8CcOQqkREREmoP4+HgccMABZi8iIiISSFQp1YRMnQqwap8zpEREREREREREGjNVSjUhrVsrkBIREWludu3ahZkzZ5q9iIiISCBRKCUiIiISwKKiotCtWzezFxEREQkkCqVEREREAlhkZKQJpbgXERERCSQKpUREREQCWGFhIXbu3Gn2IiIiIoFEoZSIiIhIAMvMzMSCBQvMXkRERCSQKJQSERERCWAJCQkYMWKE2YuIiIgEEoVSIiIiIgEsJCQEMTExZi8iIiISSBRKiYiIiASwnJwc/P3332YvIiIiEkgUSomIiIgEMA44T01N1aBzERERCTgKpUREREQCGGdJHXTQQZopJSIiIgFHoZSIiIiIiIiIiNS70Pq/ycbH5XKZfUZGRkPfFWmiSkpKzFLdkZGRCA5WFiyi14tI7WHr3qxZs3DYYYchOTlZT62IPpeJ1Br9HSM15eQrTt5SGYVSgAkLqGPHjjV+wkVERERERERExDdv4aiBygS5dhdbNZP0d9OmTYiLi0NQUFBD3x1poikxQ8/169cjPj6+oe+OSKOm14uIXjMiep8RaRz0uUxqilETA6l27dpV2S2kSikO1goORocOHWr8ZIv4i4GUQikRvV5E6oLeY0T0mhGpK3qPkZqoqkLKoeE2IiIiIiIiIiJS7xRKiYiIiIiIiIhIvVMoJVIPIiIiMGXKFLMXEb1eRPQeI9Jw9LlMRK8XaTw06FxEREREREREROqdKqVERERERERERKTeKZQSEREREREREZF6p1BKRERERERERETqnUIpkVrwwAMPICgoCJMmTfI5fv78+Rg1ahRiYmIQHx+PESNGIDc313N6amoqzjzzTHNaYmIiLrjgAmRlZenfRJrla2bLli04++yz0aZNG/OaGTp0KD744AOfy+k1I83FHXfcYV4j3lufPn08p+fl5eHyyy9HSkoKYmNjMX78eGzdutXnOtatW4ejjz4a0dHRaNWqFa6//noUFRU1wKMRadjXDN87rrjiCvTu3RtRUVHo1KkTrrzySqSnp/tch14z0lzs7j3G4XK5MHbsWHP6xx9/7HOaXi9SW0Jr7ZpEmqmFCxdi6tSpGDRoULlA6sgjj8TkyZPx1FNPITQ0FL///juCg0uzYAZSmzdvxowZM1BYWIjzzjsPF198Md56660GeCQiDfuaOeecc5CWloZPP/0ULVq0MK+DU045BYsWLcKQIUPMefSakeakf//+mDlzpucw30ccV199Nb744gu89957SEhIwMSJE3HiiSdi7ty55vTi4mITSDHknTdvnnmv4WssLCwM9913X4M8HpGGes1s2rTJbI888gj69euHtWvX4tJLLzXHvf/+++Y8es1Ic1PVe4zj8ccfN4FUWXq9SK1yiUiNZWZmunr27OmaMWOG65BDDnFdddVVntOGDRvmuvXWWyu97N9//+3iS3DhwoWe47766itXUFCQa+PGjfpXkWb3momJiXFNmzbN5/zJycmuF1980fys14w0J1OmTHHttddeFZ6WlpbmCgsLc7333nue4/755x/znjJ//nxz+Msvv3QFBwe7tmzZ4jnPc88954qPj3fl5+fXwyMQaTyvmYq8++67rvDwcFdhYaE5rNeMNCf+vF4WL17sat++vWvz5s3m/eWjjz7ynKbXi9Qmte+J7AG2TvCb6NGjR/scv23bNvz888+mXWL48OFo3bo1DjnkEMyZM8enkoote/vss4/nOF4PK6l4WZHm9Johvlbeeecd02ZRUlKCt99+27QojRw50pyu14w0N//++y/atWuHbt26mSpBtkrQL7/8YqprvV9HbLtgSxJfJ8T9wIEDzfuP44gjjkBGRgaWLFnSAI9GpOFeMxVh6x7HJzjVIXrNSHNT1eslJycHZ5xxBp555hlTcVuWXi9Sm9S+J1JD/IP5119/Na1IZa1atcrTr81S8cGDB2PatGk47LDD8Ndff6Fnz55mfg5DK58XZGgokpOTzWkizek1Q++++y5OPfVUMyOHrwXOwfnoo4/Qo0cPc7peM9KcDBs2DK+++qqZgcPWuzvvvBMHH3yweQ/hayE8PNx8seGNAZTz/sG9dyDlnO6cJtKcXjNxcXE+592xYwfuvvtuMzLBodeMNCe7e72wRZxfFh533HEVXl6vF6lNCqVEamD9+vW46qqrzCyoyMjIcqezyoMuueQSMyeKOBNn1qxZePnll3H//ffreZdmZXevGbrtttvMTCnON+BMKQ7U5EypH3/80VR8iDQnHCzr4Pw1/gHRuXNnE95yULOI+P+a4UIyDlYLsmKXs6X45aFIc1TV66Vly5b49ttvsXjx4ga9j9J8qH1PpAbYOsEWPa4OxooObj/88AOefPJJ87PzbTQ/8Hjr27evpzSWpbC8Dm9cFYmtSxWVyYo05dfMypUr8fTTT5vQlhWFe+21F6ZMmWLaW1k6TnrNSHPGqqhevXphxYoV5rVQUFBgQlxvXH3Pef/gvuxqfM5hvcdIc3vNODIzM80iNKwEYSUuB/879JqR5sz79cJAip/LeJzzmY24yqszUkGvF6lNCqVEaoB/NP/555/47bffPBv/eGY/Nn9mbzZ7tJctW+ZzueXLl5tvIeiAAw4wf1Dwj3UH3wRYZcVvK0Sa02uGswvIe3VKCgkJ8VQe6jUjzVlWVpb5I6Ft27bYe++9zR/TrL518P2GX3rwdULc8zXn/eUHKxU5Q6fsFyYiTf0141RIjRkzxrS+cpXXslW7es1Ic+b9ernpppvwxx9/+Hxmo8ceewyvvPKK+VmvF6lVtTo2XaQZK7uS2GOPPWZWOeLqSP/++69ZiS8yMtK1YsUKz3mOPPJI15AhQ1w///yza86cOWZVstNPP72BHoFIw71mCgoKXD169HAdfPDB5vXA18kjjzxiVqP84osvPJfRa0aai2uvvdb1/fffu1avXu2aO3eua/To0a4WLVq4tm3bZk6/9NJLXZ06dXJ9++23rkWLFrkOOOAAszmKiopcAwYMcI0ZM8b122+/ub7++mtXy5YtXZMnT27ARyXSMK+Z9PR0syrywIEDzfsLVxNzNr5WSK8ZaU529x5TVtnV9/R6kdqkmVIidWTSpElm5TAOCmRLHtuR+C119+7dPed58803MXHiRFNFwgoRlsWynUmkuWHVx5dffmm+nRs3bpz5xo4Dzl977TUcddRRnvPpNSPNxYYNG3D66adj586dZr7HQQcdhJ9++sn87Hxj7bxv5Ofnm5X1nn32WZ8qw88//xyXXXaZ+UY7JiYGEyZMwF133dWAj0qkYV4z33//vWdlY2fxDMfq1avRpUsXvWakWdnde8zu6D1GalMQk6lavUYREREREREREZHd0EwpERERERERERGpdwqlRERERERERESk3imUEhERERERERGReqdQSkRERERERERE6p1CKRERERERERERqXcKpUREREREREREpN4plBIRERERERERkXqnUEpEREREREREROqdQikRERGRRuTcc8/F8ccf39B3Q0RERKTOhdb9TYiIiIgIBQUFVflETJkyBU888QRcLpeeMBEREWnyFEqJiIiI1JPNmzd7fn7nnXdw++23Y9myZZ7jYmNjzSYiIiLSHKh9T0RERKSetGnTxrMlJCSYyinv4xhIlW3fGzlyJK644gpMmjQJSUlJaN26NV588UVkZ2fjvPPOQ1xcHHr06IGvvvrK57b++usvjB071lwnL3P22Wdjx44d+rcWERGRRkOhlIiIiEgj99prr6FFixZYsGCBCaguu+wynHzyyRg+fDh+/fVXjBkzxoROOTk55vxpaWkYNWoUhgwZgkWLFuHrr7/G1q1bccoppzT0QxERERHxUCglIiIi0sjttddeuPXWW9GzZ09MnjwZkZGRJqS66KKLzHFsA9y5cyf++OMPc/6nn37aBFL33Xcf+vTpY35++eWX8d1332H58uUN/XBEREREDM2UEhEREWnkBg0a5Pk5JCQEKSkpGDhwoOc4tufRtm3bzP733383AVRF86lWrlyJXr161cv9FhEREamKQikRERGRRi4sLMznMGdReR/nrOpXUlJi9llZWRg3bhwefPDBctfVtm3bOr+/IiIiIv5QKCUiIiLSxAwdOhQffPABunTpgtBQfdwTERGRxkkzpURERESamMsvvxypqak4/fTTsXDhQtOyN336dLNaX3FxcUPfPRERERFDoZSIiIhIE9OuXTvMnTvXBFBcmY/zpyZNmoTExEQEB+vjn4iIiDQOQS6Xy9XQd0JERERERERERJoXfVUmIiIiIiIiIiL1TqGUiIiIiIiIiIjUO4VSIiIiIiIiIiJS7xRKiYiIiIiIiIhIvVMoJSIiIiIiIiIi9U6hlIiIiIiIiIiI1DuFUiIiIiIiIiIiUu8USomIiIiIiIiISL1TKCUiIiIiIiIiIvVOoZSIiIiIiIiIiNQ7hVIiIiIiIiIiIlLvFEqJiIiIiIiIiEi9UyglIiIiIiIiIiL1TqGUiIiIiIiIiIjUO4VSIiIiIiIiIiJS7xRKiYiIiIiIiIhIvVMoJSIiIiIiIiIi9U6hlIiIiNS6c889F126dNEzWw9effVVBAUFYc2aNZ7jRo4caTYRERGRxkyhlIiIiPiFwYc/2/fff+/X5ePj43HIIYfgiy++aLB/gezsbNx9990YNGgQoqOjkZCQgIMPPhivv/46XC4XGpP77rsPH3/8cYOFjJX9e3/99ddo6hryuRcREWnKglyN7ROXiIiINEpvvPGGz+Fp06ZhxowZJsDxdvjhhyM5ORklJSWIiIjwHM8Ag6edc845JvBZu3YtnnvuOWzevBlfffUVjjjiCNSnrVu34rDDDsM///yD0047zQRkeXl5+OCDDzB79mycccYZ5rEFBzeO7/BiY2Nx0kknmcoob8XFxSgsLDTPNZ9jcqqkKgsIaxJKvf3223jppZfKncbnsG3btmjKKnvuRUREZM+E7uHlRUREpJk466yzfA7/9NNPJpQqe3xVevXq5XP+8ePHo1+/fnjiiSfqPZSaMGGCCaQ++ugjHHvssZ7jr7zySlx//fV45JFHMHjwYPNzYxYSEmK2uhYaGlqtf+vqyMnJMZVqIiIi0rw0jq/+REREpFnOlOrbty9atGiBlStX+hyfn5+PKVOmoEePHqYCqGPHjrjhhhvM8d6hUmRkpAmWvDHcSkpKwqZNmyq9XQZq06dPN/fTO5By3H///ejZsyceeOAB5ObmeqqOKmpP5CwnHu9dRfPHH3+Y6+7WrZu5j23atMH555+PnTt3+lz2jjvuMJddsWKFOX9iYqJpITzvvPNMUOPgedhq+Nprr3na5nj+ymZKVcSf53RPPfvss+jfv7+5/nbt2uHyyy9HWlqaz3lYxTVgwAD88ssvGDFihAmjbr755mrfR1bu7bfffuby/PfmdX3zzTee0z/55BMcffTR5n7wurp3725aNVlZ5u3ff/814Sj/jfhv1aFDB1M5l56evtvnXkRERPaMKqVERESkwfAP/127dpnAwMG2PwZFc+bMwcUXX2yCqz///BOPPfYYli9f7pntw+qqb7/91oRT8+fPN9VCU6dONcEE2+4YRlTms88+M3u2ElZWFcT2vTvvvBPz5s0zLWrVwQqyVatWmXCJYceSJUvwwgsvmD0DMafNznHKKaega9euJgz79ddfTZtcq1at8OCDD5rT+XguvPBCE8LwOSHv52x3/H1Od2fHjh0+h8PCwkyI5gRsfL5Gjx6Nyy67DMuWLTPtmQsXLsTcuXPNeR0M58aOHWvCH1ZftW7dulr3kbfD2xs+fDjuuusuhIeH4+effza/D2PGjPGEdWy7u+aaa8yep91+++3IyMjAww8/bM5TUFBgQkyGXldccYX5t9q4cSM+//xzE6bxse3pcy8iIiJV4EwpERERkeq6/PLLOZeywtMmTJjg6ty5s89xPO8FF1zg2r59u2vbtm2uRYsWuY488khz/MMPP+w53+uvv+4KDg52/fjjjz6Xf/755815586d6zlu+vTp5rh77rnHtWrVKldsbKzr+OOP3+1953l4uV27dlV6ng8//NCc58knnzSHv/vuO3OYe2+rV682x7/yyiue43Jycspd3//93/+Z882ePdtz3JQpU8xx559/vs95TzjhBFdKSorPcTExMeZ5LYu3y+vg/XAccsghZqvJc1oR3i7PV3ZzboP/nuHh4a4xY8a4iouLPZd7+umnzflefvlln/vG43jb3vy9j//++685H58j79uikpKSKv8NLrnkEld0dLQrLy/PHF68eLG57vfee6/Kx1/Zcy8iIiJ7Ru17IiIiUm/+97//oWXLlqYKaJ999sGsWbNMexarWRzvvfeeqZLp06ePqcxxtlGjRpnTv/vuO895WRVzySWXmGqZE0880bRfsVpqdzIzM80+Li6u0vM4pznnrY6oqCjPzxyezvu///77m8OshCrr0ksv9TnMFQBZTcSqntpQnee0MnxuWQHmvf33v/81p82cOdNUHU2aNMlnMPxFF11kVlksu8Ii2+lYRVaT+8iKKVZVseqp7BB67wo0738D/hvyuvi8si1y6dKl5ninyoutnN7tkiIiIlI/1L4nIiIi9ea4447DxIkTTYDBtq777rvPhAHe4QJn/HBOFMOrimzbts3nMAeSc37Qb7/9hrfeessEXrvjHThxjlNFnDDKn+srKzU11bSYccW6svfXmVXkrVOnTj6HOSOJ2NrIUGdPVfc5rQjbI9maVxGupEi9e/f2OZ5tdZyr5ZzuaN++vTmtJveR88f4+8IB+VVhq+Stt95q2vbKhnvOvwFbJhmIPvroo3jzzTdNaMUWQrYUOoGViIiI1B2FUiIiIlJvOETaCTaOOuooM+ScIdWhhx5qKp2IVTADBw40QUFFOPza2+LFiz2BBWcQnX766bu9Hww0WHHDgeQckF0RnkYMVajsHChH2cHZzowozqLiyn1cwY8zjfi4jjzySLMvq7LV82zX456r7nNa17yrmOriPnIe1CGHHGICPVbRcQYUK71YpXbjjTf6/Buw2ouDyxlsch4ZV1/kbC/O/uLvq4iIiNQdhVIiIiLSYNh6x0HWrGg54YQTTPDDAOH33383w8UrC4IcXBWNbWAMmTj0+qGHHjLXs++++1Z5uXHjxpkqrWnTplUYSjFoYtUVB3A7pzvVS2VXkytbBcTqJrYlslKKLWbelUB7YnfPRVWq85zWROfOnc2ew82dEI9YEbd69epKK6xqch95PoZKf//9twn8KsIVEtn++OGHH/r8+/K+VIRhGDf+HjJMPPDAA/H888/jnnvuMafXxXMmIiIigGZKiYiISIPhKnfXXnutadtipYpTZcQV0F588cVy58/NzTVBlINVL+vWrcNrr71mKmy6dOliVuPjampV4XwnzqN65ZVXzEprZd1yyy1mxTfOu+J9dIIXVjTNnj3b57zPPvtshVVPZaucHn/8ceyJmJiYcoGYv6rznNYEQye24z355JM+j5szxNgqd/TRR9fafTz++ONN+x4roMpWnTm3XdG/AQOysv9WbOsrKiryOY7hFK/f+3doT557ERERqZwqpURERKRBsXWKFUUPPvigCRzOPvtsvPvuu2b4N4dbs2qFlUscTs3jOZSaQ9I5K4ghw5QpUzB06FBzXQyZRo4cidtuu81UTVWFVVIcos05V2eccYaZJ8QggtU1rLThXKGrr77ac37OGDr55JPx1FNPeSq6GGiVncfEljFW5/D2CwsLzfwktoVVVqXjr7333tsMFGf41q5dOzMPadiwYX5d1t/ntKY4B2ry5MmmOowtipzLxKop/vuwao3PZW3dxx49epjQ8O677zb/Zmz75OB0zijj88LWO1bNsbKNASXb8fjv9frrr5cLCvk7xPZR/rv26tXLBFQ8H0Ot8ePH18pzLyIiIlXYw9X7REREpJm6/PLL+Rd+hadNmDDB1blzZ5/jeF5epiJ33HGHOf27774zhwsKClwPPvigq3///q6IiAhXUlKSa++993bdeeedrvT0dFdGRoa5/qFDh7oKCwt9ruvqq692BQcHu+bPn7/bx5CZmWmuk7cTGRlp7gO32267rcLzb9++3TV+/HhXdHS0uU+XXHKJ66+//jKXeeWVVzzn27Bhg+uEE05wJSYmuhISElwnn3yya9OmTeZ8U6ZM8ZyPP/M4Xq83XhePX716tee4pUuXukaMGOGKiooyp/E5ruy8hxxyiNm87e45rQpvKyYmZrfP59NPP+3q06ePKywszNW6dWvXZZdd5tq1a5fPeXi/eB8qUp37+PLLL7uGDBniOR+vd8aMGZ7T586d69p///3N89WuXTvXDTfc4Jo+fbrP79mqVatc559/vqt79+7m3z85Odl16KGHumbOnOlzW5U99yIiIrJngvifqkIrERERkeaC7WOssmHFzPz588utiiciIiIitUczpURERETc2Gr39ddfIy8vD2PHjjVDy0VERESkbqhSSkRERERERERE6p0qpUREREREREREpN4plBIRERERERERkXqnUEpEREREREREROqdQikREREREREREal3ofV/k41PSUkJNm3ahLi4OAQFBTX03RERERERERERCVgulwuZmZlo164dgoMrr4dSKAWYQKpjx471+e8jIiIiIiIiItKkrV+/Hh06dKj0dIVSgKmQcp6s+Pj4+vvXkWZVjbd9+3a0bNmyypRYRPR6Eamu9PR0zJ07FwceeCASEhL0BIroc5lIrdHfMVJTGRkZpvjHyVsqo1AK8LTsMZBSKCV19T/zvLw88/ulUEpErxeR2paYmGgCKX2OEdHnMpHapL9jZE/tbkSSSjZEREREAlhsbCwGDx5s9iIiIiKBRKGUiIiISIAPEi0sLDR7ERERkUCiUEpEREQkgO3atQszZ840exEREZFAoplS1eilLSgoqNt/DWnSvz/8FptzpWo6Uyo8PFzzqEREpBy174mIiEigUijlB4ZRq1evNsGCSE2wpYK/P5mZmbsd9FYZhlldu3Y14ZSIiIiD7wtt27bV+4OIiIgEHIVSfoQJmzdvRkhIiFnOUCunSU1/j4qKihAaGlqjUIqB1qZNm8zvYqdOnWocbImISNOTn5+PDRs2mNX3oqKiGvruiIiIiPhNodRuMEjIyclBu3btEB0d7f8zK1KLoRS1bNnSBFO8nrCwMD2/IiJiZGdn488//0SXLl0USomIiEhA0aDz3SguLjZ7tUxJQ3N+B53fSREREUpKSsIRRxxh9iIiIiKBRKGUn9QuJQ1Nv4MiIlLZ+wPHC+h9QkRERAKNQikRERGRAMZFNH755RezFxEREQkkmiklfuOsikmTJpnNH99//z0OPfRQ7Nq1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",
+ "text/plain": [
+ ""
+ ]
+ },
+ "metadata": {},
+ "output_type": "display_data"
+ }
+ ],
+ "source": [
+ "# Plot quantile forecasts for all models\n",
+ "fig, axes = plt.subplots(3, 1, figsize=(12, 12))\n",
+ "\n",
+ "# Plot only last 'horizon' points of training data\n",
+ "train_context_start = max(0, train_size - horizon)\n",
+ "train_context_indices = np.arange(train_context_start, train_size)\n",
+ "\n",
+ "for idx, (name, quantiles) in enumerate(quantile_models.items()):\n",
+ " ax = axes[idx]\n",
+ "\n",
+ " # Plot training data (last horizon points)\n",
+ " ax.plot(\n",
+ " train_context_indices, train_data[0, train_context_start:, 0], label=\"Training Data\", color=\"blue\", linewidth=2\n",
+ " )\n",
+ "\n",
+ " # Plot test data\n",
+ " ax.plot(test_indices, test_data[0, :, 0], label=\"Test Data\", color=\"black\", linewidth=2, linestyle=\"--\")\n",
+ "\n",
+ " # Plot median (index 2 for 0.5 quantile)\n",
+ " median_idx = 2\n",
+ " ax.plot(test_indices, quantiles[0, :, median_idx, 0], label=\"Median Forecast\", color=colors[name], linewidth=2)\n",
+ "\n",
+ " # Plot prediction intervals\n",
+ " # 80% interval (0.1 to 0.9)\n",
+ " ax.fill_between(\n",
+ " test_indices,\n",
+ " quantiles[0, :, 0, 0], # 0.1 quantile\n",
+ " quantiles[0, :, 4, 0], # 0.9 quantile\n",
+ " alpha=0.2,\n",
+ " color=colors[name],\n",
+ " label=\"80% Interval\",\n",
+ " )\n",
+ "\n",
+ " # 50% interval (0.25 to 0.75)\n",
+ " ax.fill_between(\n",
+ " test_indices,\n",
+ " quantiles[0, :, 1, 0], # 0.25 quantile\n",
+ " quantiles[0, :, 3, 0], # 0.75 quantile\n",
+ " alpha=0.3,\n",
+ " color=colors[name],\n",
+ " label=\"50% Interval\",\n",
+ " )\n",
+ "\n",
+ " ax.axvline(x=train_size, color=\"gray\", linestyle=\":\", linewidth=1, alpha=0.7)\n",
+ " ax.set_xlabel(\"Time\")\n",
+ " ax.set_ylabel(\"Value\")\n",
+ " ax.set_title(f\"{name} Quantile Forecast\")\n",
+ " ax.legend(loc=\"best\")\n",
+ " ax.grid(True, alpha=0.3)\n",
+ "\n",
+ "plt.tight_layout()\n",
+ "plt.show()"
+ ]
+ },
+ {
+ "cell_type": "markdown",
+ "metadata": {},
+ "source": [
+ "## Summary\n",
+ "\n",
+ "This notebook demonstrated how to use the FAIM platform to:\n",
+ "\n",
+ "1. **Point Forecasting**: Compare FlowState, Chronos2, and TiRex models\n",
+ "2. **Evaluation Metrics**: Compute MSE and MASE for point forecasts\n",
+ "3. **Quantile Forecasting**: Generate probabilistic forecasts with uncertainty intervals"
+ ]
+ }
+ ],
+ "metadata": {
+ "kernelspec": {
+ "display_name": "Python 3",
+ "language": "python",
+ "name": "python3"
+ },
+ "language_info": {
+ "codemirror_mode": {
+ "name": "ipython",
+ "version": 3
+ },
+ "file_extension": ".py",
+ "mimetype": "text/x-python",
+ "name": "python",
+ "nbconvert_exporter": "python",
+ "pygments_lexer": "ipython3",
+ "version": "3.12.0"
+ }
+ },
+ "nbformat": 4,
+ "nbformat_minor": 4
+}
diff --git a/faim_client/api/forecast/forecast_v1_ts_forecast_model_name_model_version_post.py b/faim_client/api/forecast/forecast_v1_ts_forecast_model_name_model_version_post.py
index 656976a..8d60530 100644
--- a/faim_client/api/forecast/forecast_v1_ts_forecast_model_name_model_version_post.py
+++ b/faim_client/api/forecast/forecast_v1_ts_forecast_model_name_model_version_post.py
@@ -1,11 +1,12 @@
from http import HTTPStatus
from io import BytesIO
-from typing import Any, cast
+from typing import Any
import httpx
from ... import errors
from ...client import AuthenticatedClient, Client
+from ...models.error_response import ErrorResponse
from ...models.model_name import ModelName
from ...types import File, Response
@@ -33,32 +34,52 @@ def _get_kwargs(
def _parse_response(
*, client: AuthenticatedClient | Client, response: httpx.Response
-) -> Any | File | None:
+) -> ErrorResponse | File | None:
if response.status_code == 200:
response_200 = File(payload=BytesIO(response.content))
return response_200
if response.status_code == 401:
- response_401 = cast(Any, None)
+ response_401 = ErrorResponse.from_dict(response.json())
+
return response_401
+ if response.status_code == 402:
+ response_402 = ErrorResponse.from_dict(response.json())
+
+ return response_402
+
if response.status_code == 404:
- response_404 = cast(Any, None)
+ response_404 = ErrorResponse.from_dict(response.json())
+
return response_404
if response.status_code == 413:
- response_413 = cast(Any, None)
+ response_413 = ErrorResponse.from_dict(response.json())
+
return response_413
if response.status_code == 422:
- response_422 = cast(Any, None)
+ response_422 = ErrorResponse.from_dict(response.json())
+
return response_422
if response.status_code == 500:
- response_500 = cast(Any, None)
+ response_500 = ErrorResponse.from_dict(response.json())
+
return response_500
+ if response.status_code == 503:
+ response_503 = ErrorResponse.from_dict(response.json())
+
+ return response_503
+
+ if response.status_code == 504:
+ response_504 = ErrorResponse.from_dict(response.json())
+
+ return response_504
+
if client.raise_on_unexpected_status:
raise errors.UnexpectedStatus(response.status_code, response.content)
else:
@@ -67,7 +88,7 @@ def _parse_response(
def _build_response(
*, client: AuthenticatedClient | Client, response: httpx.Response
-) -> Response[Any | File]:
+) -> Response[ErrorResponse | File]:
return Response(
status_code=HTTPStatus(response.status_code),
content=response.content,
@@ -82,35 +103,126 @@ def sync_detailed(
*,
client: AuthenticatedClient | Client,
body: File,
-) -> Response[Any | File]:
- r"""Generate model forecast
+) -> Response[ErrorResponse | File]:
+ r"""Generate time series forecast
Generate time series forecasts using the specified Triton model.
- **Authentication**: Requires valid API key in Authorization header as `Bearer `
+ ## Authentication
+ Requires valid API key in Authorization header as `Bearer `
+ - API keys can be created via `/v1/user/api-keys/create` endpoint
+ - Keys must follow the format: `sk--`
+ - Invalid or expired keys return 401 INVALID_API_KEY
- **Request Format**: Apache Arrow IPC Stream (`application/vnd.apache.arrow.stream`)
+ ## Request Format
+ Apache Arrow IPC Stream (`application/vnd.apache.arrow.stream`)
- Large arrays (x, padding_mask, etc.) sent as Arrow columns
- Small parameters (horizon, quantiles, etc.) sent in schema metadata
-
- **Required Inputs**:
- - `x`: Time series data (numpy array)
- - `horizon`: Forecast horizon length (integer, in metadata)
- - `output_type`: Output type - \"point\", \"quantiles\", or \"samples\" (string, in metadata)
-
- **Model-Specific Inputs**:
-
- *FlowState*:
- - `scale_factor` (optional): Scaling factor (float)
- - `prediction_type` (optional): Prediction type (string)
-
- *ToTo*:
- - `padding_mask`: Mask for padded values (numpy array)
- - `id_mask`: Identifier mask (numpy array)
- - `num_samples` (optional): Number of samples (integer)
- - `quantiles` (optional): Quantile levels (list of floats)
-
- **Response**: Arrow IPC stream with predictions and metadata
+ - Request timeout: 20 seconds (configurable on server)
+ - Maximum request size: 100MB (configurable on server)
+
+ ## Required Inputs
+ - `x`: Time series data (numpy array, 1D or 2D)
+ - `horizon`: Forecast horizon length (integer, positive, in metadata)
+ - `output_type`: Output type (string, in metadata)
+ - Valid values: `\"point\"`, `\"quantiles\"`, `\"samples\"`
+ - Determines output format in response
+
+ ## Model-Specific Inputs
+
+ ### FlowState Model
+ - `scale_factor` (float, optional): Scaling factor for input time series (default: None)
+ - `prediction_type` (string, optional): Type of prediction (default: None)
+
+ ### Chronos2 Model
+ - `quantiles` (float array, optional): Quantile levels for quantiles output (list of floats,
+ 0.0-1.0)
+
+ ### TiRex Model
+
+ ## Response Format
+ Successful responses return Apache Arrow IPC stream with:
+
+ ### Common Response Metadata (All Models)
+ - `model_name` (string): Name of model used
+ - `model_version` (string): Version of model used
+ - `transaction_id` (string): Unique identifier for billing tracking
+ - `cost_amount` (string): Amount charged for this inference (in currency units)
+ - `cost_currency` (string): Currency code (e.g., \"USD\")
+
+ ### Response Output Arrays by Model & output_type
+
+ **FlowState:**
+ - `output_type=\"point\"`: Single point forecast array (shape: batch x horizon)
+ - `output_type=\"quantiles\"`: Quantile forecasts array (shape: batch x horizon x num_quantiles)
+
+ **Chronos2:**
+ - `output_type=\"point\"`: Point forecast array (shape: batch x horizon)
+ - `output_type=\"quantiles\"`: Quantile predictions array (shape: batch x horizon x
+ num_quantiles)
+
+ **TiRex:**
+ - `output_type=\"point\"`: Point forecast array (shape: batch x horizon)
+ - `output_type=\"quantiles\"`: Quantile predictions array (shape: batch x horizon x
+ num_quantiles)
+
+ ## Error Handling
+ All errors return `ErrorResponse` (HTTP JSON) with:
+ - `error_code`: Machine-readable code for programmatic handling (see ErrorCode enum)
+ - `message`: Human-readable error message
+ - `detail`: Optional detailed explanation
+ - `request_id`: Request identifier for debugging
+
+ ### Error Categories
+
+ **Authentication & Authorization (401, 403)**
+ - AUTHENTICATION_REQUIRED: Missing or malformed Authorization header
+ - AUTHENTICATION_FAILED: Bearer format invalid
+ - INVALID_API_KEY: API key not found, expired, or revoked
+ - AUTHORIZATION_FAILED: Valid credentials but insufficient permissions
+
+ **Validation Errors (422)**
+ - MISSING_REQUIRED_FIELD: Required input missing (x, horizon, output_type)
+ - INVALID_PARAMETER: Parameter out of valid range or invalid type
+ - INVALID_SHAPE: Input tensor shape incompatible with model
+ - INVALID_DTYPE: Input data type incorrect for model
+ - INVALID_VALUE_RANGE: Input values outside acceptable range
+ - VALIDATION_ERROR: Invalid Arrow format or other validation failure
+
+ **Resource Errors (404, 402)**
+ - MODEL_NOT_FOUND: Model or version doesn't exist
+ - PRICING_NOT_FOUND: Pricing configuration missing for model/version
+ - INSUFFICIENT_FUNDS: User billing account balance insufficient
+ - REQUEST_TOO_LARGE: Request payload exceeds 100MB limit
+
+ **Inference Errors (500, 503, 504)**
+ - INFERENCE_ERROR: Model inference failed (check detail for reason)
+ - TIMEOUT_ERROR: Inference exceeded 20 second deadline
+ - OUT_OF_MEMORY: GPU/CPU memory exhausted during inference
+ - RESOURCE_EXHAUSTED: Compute resources temporarily unavailable
+ - MODEL_INITIALIZATION_ERROR: Model failed to load or initialize
+
+ **System Errors (500, 503)**
+ - TRITON_CONNECTION_ERROR: Cannot connect to Triton server
+ - DATABASE_ERROR: Database operation failed
+ - INTERNAL_SERVER_ERROR: Unexpected server error
+ - BILLING_TRANSACTION_FAILED: Billing system error (insufficient funds, etc.)
+
+ ### Retry Strategy
+ **Retryable Errors** (transient, safe to retry with exponential backoff):
+ - TIMEOUT_ERROR (504): Retry after 2-5 seconds
+ - OUT_OF_MEMORY (503): Retry after 5-10 seconds or reduce batch size
+ - RESOURCE_EXHAUSTED (503): Retry after 5-10 seconds
+ - TRITON_CONNECTION_ERROR (503): Retry after 5-10 seconds
+ - DATABASE_ERROR (500): Retry after 2-5 seconds
+
+ **Non-Retryable Errors** (permanent failures, don't retry):
+ - All 401/403 auth errors: Fix credentials and resubmit
+ - All 404 errors: Resource doesn't exist, fix request
+ - All 422 validation errors: Fix input data and resubmit
+ - INSUFFICIENT_FUNDS (402): Add credit to account
+ - REQUEST_TOO_LARGE (413): Reduce payload size
+ - INFERENCE_ERROR (500): Check error detail, may require model update
Args:
model_name (ModelName): Available model names for inference.
@@ -122,7 +234,7 @@ def sync_detailed(
httpx.TimeoutException: If the request takes longer than Client.timeout.
Returns:
- Response[Union[Any, File]]
+ Response[Union[ErrorResponse, File]]
"""
kwargs = _get_kwargs(
@@ -144,35 +256,126 @@ def sync(
*,
client: AuthenticatedClient | Client,
body: File,
-) -> Any | File | None:
- r"""Generate model forecast
+) -> ErrorResponse | File | None:
+ r"""Generate time series forecast
Generate time series forecasts using the specified Triton model.
- **Authentication**: Requires valid API key in Authorization header as `Bearer `
+ ## Authentication
+ Requires valid API key in Authorization header as `Bearer `
+ - API keys can be created via `/v1/user/api-keys/create` endpoint
+ - Keys must follow the format: `sk--`
+ - Invalid or expired keys return 401 INVALID_API_KEY
- **Request Format**: Apache Arrow IPC Stream (`application/vnd.apache.arrow.stream`)
+ ## Request Format
+ Apache Arrow IPC Stream (`application/vnd.apache.arrow.stream`)
- Large arrays (x, padding_mask, etc.) sent as Arrow columns
- Small parameters (horizon, quantiles, etc.) sent in schema metadata
-
- **Required Inputs**:
- - `x`: Time series data (numpy array)
- - `horizon`: Forecast horizon length (integer, in metadata)
- - `output_type`: Output type - \"point\", \"quantiles\", or \"samples\" (string, in metadata)
-
- **Model-Specific Inputs**:
-
- *FlowState*:
- - `scale_factor` (optional): Scaling factor (float)
- - `prediction_type` (optional): Prediction type (string)
-
- *ToTo*:
- - `padding_mask`: Mask for padded values (numpy array)
- - `id_mask`: Identifier mask (numpy array)
- - `num_samples` (optional): Number of samples (integer)
- - `quantiles` (optional): Quantile levels (list of floats)
-
- **Response**: Arrow IPC stream with predictions and metadata
+ - Request timeout: 20 seconds (configurable on server)
+ - Maximum request size: 100MB (configurable on server)
+
+ ## Required Inputs
+ - `x`: Time series data (numpy array, 1D or 2D)
+ - `horizon`: Forecast horizon length (integer, positive, in metadata)
+ - `output_type`: Output type (string, in metadata)
+ - Valid values: `\"point\"`, `\"quantiles\"`, `\"samples\"`
+ - Determines output format in response
+
+ ## Model-Specific Inputs
+
+ ### FlowState Model
+ - `scale_factor` (float, optional): Scaling factor for input time series (default: None)
+ - `prediction_type` (string, optional): Type of prediction (default: None)
+
+ ### Chronos2 Model
+ - `quantiles` (float array, optional): Quantile levels for quantiles output (list of floats,
+ 0.0-1.0)
+
+ ### TiRex Model
+
+ ## Response Format
+ Successful responses return Apache Arrow IPC stream with:
+
+ ### Common Response Metadata (All Models)
+ - `model_name` (string): Name of model used
+ - `model_version` (string): Version of model used
+ - `transaction_id` (string): Unique identifier for billing tracking
+ - `cost_amount` (string): Amount charged for this inference (in currency units)
+ - `cost_currency` (string): Currency code (e.g., \"USD\")
+
+ ### Response Output Arrays by Model & output_type
+
+ **FlowState:**
+ - `output_type=\"point\"`: Single point forecast array (shape: batch x horizon)
+ - `output_type=\"quantiles\"`: Quantile forecasts array (shape: batch x horizon x num_quantiles)
+
+ **Chronos2:**
+ - `output_type=\"point\"`: Point forecast array (shape: batch x horizon)
+ - `output_type=\"quantiles\"`: Quantile predictions array (shape: batch x horizon x
+ num_quantiles)
+
+ **TiRex:**
+ - `output_type=\"point\"`: Point forecast array (shape: batch x horizon)
+ - `output_type=\"quantiles\"`: Quantile predictions array (shape: batch x horizon x
+ num_quantiles)
+
+ ## Error Handling
+ All errors return `ErrorResponse` (HTTP JSON) with:
+ - `error_code`: Machine-readable code for programmatic handling (see ErrorCode enum)
+ - `message`: Human-readable error message
+ - `detail`: Optional detailed explanation
+ - `request_id`: Request identifier for debugging
+
+ ### Error Categories
+
+ **Authentication & Authorization (401, 403)**
+ - AUTHENTICATION_REQUIRED: Missing or malformed Authorization header
+ - AUTHENTICATION_FAILED: Bearer format invalid
+ - INVALID_API_KEY: API key not found, expired, or revoked
+ - AUTHORIZATION_FAILED: Valid credentials but insufficient permissions
+
+ **Validation Errors (422)**
+ - MISSING_REQUIRED_FIELD: Required input missing (x, horizon, output_type)
+ - INVALID_PARAMETER: Parameter out of valid range or invalid type
+ - INVALID_SHAPE: Input tensor shape incompatible with model
+ - INVALID_DTYPE: Input data type incorrect for model
+ - INVALID_VALUE_RANGE: Input values outside acceptable range
+ - VALIDATION_ERROR: Invalid Arrow format or other validation failure
+
+ **Resource Errors (404, 402)**
+ - MODEL_NOT_FOUND: Model or version doesn't exist
+ - PRICING_NOT_FOUND: Pricing configuration missing for model/version
+ - INSUFFICIENT_FUNDS: User billing account balance insufficient
+ - REQUEST_TOO_LARGE: Request payload exceeds 100MB limit
+
+ **Inference Errors (500, 503, 504)**
+ - INFERENCE_ERROR: Model inference failed (check detail for reason)
+ - TIMEOUT_ERROR: Inference exceeded 20 second deadline
+ - OUT_OF_MEMORY: GPU/CPU memory exhausted during inference
+ - RESOURCE_EXHAUSTED: Compute resources temporarily unavailable
+ - MODEL_INITIALIZATION_ERROR: Model failed to load or initialize
+
+ **System Errors (500, 503)**
+ - TRITON_CONNECTION_ERROR: Cannot connect to Triton server
+ - DATABASE_ERROR: Database operation failed
+ - INTERNAL_SERVER_ERROR: Unexpected server error
+ - BILLING_TRANSACTION_FAILED: Billing system error (insufficient funds, etc.)
+
+ ### Retry Strategy
+ **Retryable Errors** (transient, safe to retry with exponential backoff):
+ - TIMEOUT_ERROR (504): Retry after 2-5 seconds
+ - OUT_OF_MEMORY (503): Retry after 5-10 seconds or reduce batch size
+ - RESOURCE_EXHAUSTED (503): Retry after 5-10 seconds
+ - TRITON_CONNECTION_ERROR (503): Retry after 5-10 seconds
+ - DATABASE_ERROR (500): Retry after 2-5 seconds
+
+ **Non-Retryable Errors** (permanent failures, don't retry):
+ - All 401/403 auth errors: Fix credentials and resubmit
+ - All 404 errors: Resource doesn't exist, fix request
+ - All 422 validation errors: Fix input data and resubmit
+ - INSUFFICIENT_FUNDS (402): Add credit to account
+ - REQUEST_TOO_LARGE (413): Reduce payload size
+ - INFERENCE_ERROR (500): Check error detail, may require model update
Args:
model_name (ModelName): Available model names for inference.
@@ -184,7 +387,7 @@ def sync(
httpx.TimeoutException: If the request takes longer than Client.timeout.
Returns:
- Union[Any, File]
+ Union[ErrorResponse, File]
"""
return sync_detailed(
@@ -201,35 +404,126 @@ async def asyncio_detailed(
*,
client: AuthenticatedClient | Client,
body: File,
-) -> Response[Any | File]:
- r"""Generate model forecast
+) -> Response[ErrorResponse | File]:
+ r"""Generate time series forecast
Generate time series forecasts using the specified Triton model.
- **Authentication**: Requires valid API key in Authorization header as `Bearer `
+ ## Authentication
+ Requires valid API key in Authorization header as `Bearer `
+ - API keys can be created via `/v1/user/api-keys/create` endpoint
+ - Keys must follow the format: `sk--`
+ - Invalid or expired keys return 401 INVALID_API_KEY
- **Request Format**: Apache Arrow IPC Stream (`application/vnd.apache.arrow.stream`)
+ ## Request Format
+ Apache Arrow IPC Stream (`application/vnd.apache.arrow.stream`)
- Large arrays (x, padding_mask, etc.) sent as Arrow columns
- Small parameters (horizon, quantiles, etc.) sent in schema metadata
-
- **Required Inputs**:
- - `x`: Time series data (numpy array)
- - `horizon`: Forecast horizon length (integer, in metadata)
- - `output_type`: Output type - \"point\", \"quantiles\", or \"samples\" (string, in metadata)
-
- **Model-Specific Inputs**:
-
- *FlowState*:
- - `scale_factor` (optional): Scaling factor (float)
- - `prediction_type` (optional): Prediction type (string)
-
- *ToTo*:
- - `padding_mask`: Mask for padded values (numpy array)
- - `id_mask`: Identifier mask (numpy array)
- - `num_samples` (optional): Number of samples (integer)
- - `quantiles` (optional): Quantile levels (list of floats)
-
- **Response**: Arrow IPC stream with predictions and metadata
+ - Request timeout: 20 seconds (configurable on server)
+ - Maximum request size: 100MB (configurable on server)
+
+ ## Required Inputs
+ - `x`: Time series data (numpy array, 1D or 2D)
+ - `horizon`: Forecast horizon length (integer, positive, in metadata)
+ - `output_type`: Output type (string, in metadata)
+ - Valid values: `\"point\"`, `\"quantiles\"`, `\"samples\"`
+ - Determines output format in response
+
+ ## Model-Specific Inputs
+
+ ### FlowState Model
+ - `scale_factor` (float, optional): Scaling factor for input time series (default: None)
+ - `prediction_type` (string, optional): Type of prediction (default: None)
+
+ ### Chronos2 Model
+ - `quantiles` (float array, optional): Quantile levels for quantiles output (list of floats,
+ 0.0-1.0)
+
+ ### TiRex Model
+
+ ## Response Format
+ Successful responses return Apache Arrow IPC stream with:
+
+ ### Common Response Metadata (All Models)
+ - `model_name` (string): Name of model used
+ - `model_version` (string): Version of model used
+ - `transaction_id` (string): Unique identifier for billing tracking
+ - `cost_amount` (string): Amount charged for this inference (in currency units)
+ - `cost_currency` (string): Currency code (e.g., \"USD\")
+
+ ### Response Output Arrays by Model & output_type
+
+ **FlowState:**
+ - `output_type=\"point\"`: Single point forecast array (shape: batch x horizon)
+ - `output_type=\"quantiles\"`: Quantile forecasts array (shape: batch x horizon x num_quantiles)
+
+ **Chronos2:**
+ - `output_type=\"point\"`: Point forecast array (shape: batch x horizon)
+ - `output_type=\"quantiles\"`: Quantile predictions array (shape: batch x horizon x
+ num_quantiles)
+
+ **TiRex:**
+ - `output_type=\"point\"`: Point forecast array (shape: batch x horizon)
+ - `output_type=\"quantiles\"`: Quantile predictions array (shape: batch x horizon x
+ num_quantiles)
+
+ ## Error Handling
+ All errors return `ErrorResponse` (HTTP JSON) with:
+ - `error_code`: Machine-readable code for programmatic handling (see ErrorCode enum)
+ - `message`: Human-readable error message
+ - `detail`: Optional detailed explanation
+ - `request_id`: Request identifier for debugging
+
+ ### Error Categories
+
+ **Authentication & Authorization (401, 403)**
+ - AUTHENTICATION_REQUIRED: Missing or malformed Authorization header
+ - AUTHENTICATION_FAILED: Bearer format invalid
+ - INVALID_API_KEY: API key not found, expired, or revoked
+ - AUTHORIZATION_FAILED: Valid credentials but insufficient permissions
+
+ **Validation Errors (422)**
+ - MISSING_REQUIRED_FIELD: Required input missing (x, horizon, output_type)
+ - INVALID_PARAMETER: Parameter out of valid range or invalid type
+ - INVALID_SHAPE: Input tensor shape incompatible with model
+ - INVALID_DTYPE: Input data type incorrect for model
+ - INVALID_VALUE_RANGE: Input values outside acceptable range
+ - VALIDATION_ERROR: Invalid Arrow format or other validation failure
+
+ **Resource Errors (404, 402)**
+ - MODEL_NOT_FOUND: Model or version doesn't exist
+ - PRICING_NOT_FOUND: Pricing configuration missing for model/version
+ - INSUFFICIENT_FUNDS: User billing account balance insufficient
+ - REQUEST_TOO_LARGE: Request payload exceeds 100MB limit
+
+ **Inference Errors (500, 503, 504)**
+ - INFERENCE_ERROR: Model inference failed (check detail for reason)
+ - TIMEOUT_ERROR: Inference exceeded 20 second deadline
+ - OUT_OF_MEMORY: GPU/CPU memory exhausted during inference
+ - RESOURCE_EXHAUSTED: Compute resources temporarily unavailable
+ - MODEL_INITIALIZATION_ERROR: Model failed to load or initialize
+
+ **System Errors (500, 503)**
+ - TRITON_CONNECTION_ERROR: Cannot connect to Triton server
+ - DATABASE_ERROR: Database operation failed
+ - INTERNAL_SERVER_ERROR: Unexpected server error
+ - BILLING_TRANSACTION_FAILED: Billing system error (insufficient funds, etc.)
+
+ ### Retry Strategy
+ **Retryable Errors** (transient, safe to retry with exponential backoff):
+ - TIMEOUT_ERROR (504): Retry after 2-5 seconds
+ - OUT_OF_MEMORY (503): Retry after 5-10 seconds or reduce batch size
+ - RESOURCE_EXHAUSTED (503): Retry after 5-10 seconds
+ - TRITON_CONNECTION_ERROR (503): Retry after 5-10 seconds
+ - DATABASE_ERROR (500): Retry after 2-5 seconds
+
+ **Non-Retryable Errors** (permanent failures, don't retry):
+ - All 401/403 auth errors: Fix credentials and resubmit
+ - All 404 errors: Resource doesn't exist, fix request
+ - All 422 validation errors: Fix input data and resubmit
+ - INSUFFICIENT_FUNDS (402): Add credit to account
+ - REQUEST_TOO_LARGE (413): Reduce payload size
+ - INFERENCE_ERROR (500): Check error detail, may require model update
Args:
model_name (ModelName): Available model names for inference.
@@ -241,7 +535,7 @@ async def asyncio_detailed(
httpx.TimeoutException: If the request takes longer than Client.timeout.
Returns:
- Response[Union[Any, File]]
+ Response[Union[ErrorResponse, File]]
"""
kwargs = _get_kwargs(
@@ -261,35 +555,126 @@ async def asyncio(
*,
client: AuthenticatedClient | Client,
body: File,
-) -> Any | File | None:
- r"""Generate model forecast
+) -> ErrorResponse | File | None:
+ r"""Generate time series forecast
Generate time series forecasts using the specified Triton model.
- **Authentication**: Requires valid API key in Authorization header as `Bearer `
+ ## Authentication
+ Requires valid API key in Authorization header as `Bearer `
+ - API keys can be created via `/v1/user/api-keys/create` endpoint
+ - Keys must follow the format: `sk--`
+ - Invalid or expired keys return 401 INVALID_API_KEY
- **Request Format**: Apache Arrow IPC Stream (`application/vnd.apache.arrow.stream`)
+ ## Request Format
+ Apache Arrow IPC Stream (`application/vnd.apache.arrow.stream`)
- Large arrays (x, padding_mask, etc.) sent as Arrow columns
- Small parameters (horizon, quantiles, etc.) sent in schema metadata
-
- **Required Inputs**:
- - `x`: Time series data (numpy array)
- - `horizon`: Forecast horizon length (integer, in metadata)
- - `output_type`: Output type - \"point\", \"quantiles\", or \"samples\" (string, in metadata)
-
- **Model-Specific Inputs**:
-
- *FlowState*:
- - `scale_factor` (optional): Scaling factor (float)
- - `prediction_type` (optional): Prediction type (string)
-
- *ToTo*:
- - `padding_mask`: Mask for padded values (numpy array)
- - `id_mask`: Identifier mask (numpy array)
- - `num_samples` (optional): Number of samples (integer)
- - `quantiles` (optional): Quantile levels (list of floats)
-
- **Response**: Arrow IPC stream with predictions and metadata
+ - Request timeout: 20 seconds (configurable on server)
+ - Maximum request size: 100MB (configurable on server)
+
+ ## Required Inputs
+ - `x`: Time series data (numpy array, 1D or 2D)
+ - `horizon`: Forecast horizon length (integer, positive, in metadata)
+ - `output_type`: Output type (string, in metadata)
+ - Valid values: `\"point\"`, `\"quantiles\"`, `\"samples\"`
+ - Determines output format in response
+
+ ## Model-Specific Inputs
+
+ ### FlowState Model
+ - `scale_factor` (float, optional): Scaling factor for input time series (default: None)
+ - `prediction_type` (string, optional): Type of prediction (default: None)
+
+ ### Chronos2 Model
+ - `quantiles` (float array, optional): Quantile levels for quantiles output (list of floats,
+ 0.0-1.0)
+
+ ### TiRex Model
+
+ ## Response Format
+ Successful responses return Apache Arrow IPC stream with:
+
+ ### Common Response Metadata (All Models)
+ - `model_name` (string): Name of model used
+ - `model_version` (string): Version of model used
+ - `transaction_id` (string): Unique identifier for billing tracking
+ - `cost_amount` (string): Amount charged for this inference (in currency units)
+ - `cost_currency` (string): Currency code (e.g., \"USD\")
+
+ ### Response Output Arrays by Model & output_type
+
+ **FlowState:**
+ - `output_type=\"point\"`: Single point forecast array (shape: batch x horizon)
+ - `output_type=\"quantiles\"`: Quantile forecasts array (shape: batch x horizon x num_quantiles)
+
+ **Chronos2:**
+ - `output_type=\"point\"`: Point forecast array (shape: batch x horizon)
+ - `output_type=\"quantiles\"`: Quantile predictions array (shape: batch x horizon x
+ num_quantiles)
+
+ **TiRex:**
+ - `output_type=\"point\"`: Point forecast array (shape: batch x horizon)
+ - `output_type=\"quantiles\"`: Quantile predictions array (shape: batch x horizon x
+ num_quantiles)
+
+ ## Error Handling
+ All errors return `ErrorResponse` (HTTP JSON) with:
+ - `error_code`: Machine-readable code for programmatic handling (see ErrorCode enum)
+ - `message`: Human-readable error message
+ - `detail`: Optional detailed explanation
+ - `request_id`: Request identifier for debugging
+
+ ### Error Categories
+
+ **Authentication & Authorization (401, 403)**
+ - AUTHENTICATION_REQUIRED: Missing or malformed Authorization header
+ - AUTHENTICATION_FAILED: Bearer format invalid
+ - INVALID_API_KEY: API key not found, expired, or revoked
+ - AUTHORIZATION_FAILED: Valid credentials but insufficient permissions
+
+ **Validation Errors (422)**
+ - MISSING_REQUIRED_FIELD: Required input missing (x, horizon, output_type)
+ - INVALID_PARAMETER: Parameter out of valid range or invalid type
+ - INVALID_SHAPE: Input tensor shape incompatible with model
+ - INVALID_DTYPE: Input data type incorrect for model
+ - INVALID_VALUE_RANGE: Input values outside acceptable range
+ - VALIDATION_ERROR: Invalid Arrow format or other validation failure
+
+ **Resource Errors (404, 402)**
+ - MODEL_NOT_FOUND: Model or version doesn't exist
+ - PRICING_NOT_FOUND: Pricing configuration missing for model/version
+ - INSUFFICIENT_FUNDS: User billing account balance insufficient
+ - REQUEST_TOO_LARGE: Request payload exceeds 100MB limit
+
+ **Inference Errors (500, 503, 504)**
+ - INFERENCE_ERROR: Model inference failed (check detail for reason)
+ - TIMEOUT_ERROR: Inference exceeded 20 second deadline
+ - OUT_OF_MEMORY: GPU/CPU memory exhausted during inference
+ - RESOURCE_EXHAUSTED: Compute resources temporarily unavailable
+ - MODEL_INITIALIZATION_ERROR: Model failed to load or initialize
+
+ **System Errors (500, 503)**
+ - TRITON_CONNECTION_ERROR: Cannot connect to Triton server
+ - DATABASE_ERROR: Database operation failed
+ - INTERNAL_SERVER_ERROR: Unexpected server error
+ - BILLING_TRANSACTION_FAILED: Billing system error (insufficient funds, etc.)
+
+ ### Retry Strategy
+ **Retryable Errors** (transient, safe to retry with exponential backoff):
+ - TIMEOUT_ERROR (504): Retry after 2-5 seconds
+ - OUT_OF_MEMORY (503): Retry after 5-10 seconds or reduce batch size
+ - RESOURCE_EXHAUSTED (503): Retry after 5-10 seconds
+ - TRITON_CONNECTION_ERROR (503): Retry after 5-10 seconds
+ - DATABASE_ERROR (500): Retry after 2-5 seconds
+
+ **Non-Retryable Errors** (permanent failures, don't retry):
+ - All 401/403 auth errors: Fix credentials and resubmit
+ - All 404 errors: Resource doesn't exist, fix request
+ - All 422 validation errors: Fix input data and resubmit
+ - INSUFFICIENT_FUNDS (402): Add credit to account
+ - REQUEST_TOO_LARGE (413): Reduce payload size
+ - INFERENCE_ERROR (500): Check error detail, may require model update
Args:
model_name (ModelName): Available model names for inference.
@@ -301,7 +686,7 @@ async def asyncio(
httpx.TimeoutException: If the request takes longer than Client.timeout.
Returns:
- Union[Any, File]
+ Union[ErrorResponse, File]
"""
return (
diff --git a/faim_client/models/__init__.py b/faim_client/models/__init__.py
index 6e1d2b7..c44b98d 100644
--- a/faim_client/models/__init__.py
+++ b/faim_client/models/__init__.py
@@ -1,5 +1,15 @@
"""Contains all the data models used in inputs/outputs"""
+from .error_code import ErrorCode
+from .error_response import ErrorResponse
+from .http_validation_error import HTTPValidationError
from .model_name import ModelName
+from .validation_error import ValidationError
-__all__ = ("ModelName",)
+__all__ = (
+ "ErrorCode",
+ "ErrorResponse",
+ "HTTPValidationError",
+ "ModelName",
+ "ValidationError",
+)
diff --git a/faim_client/models/error_code.py b/faim_client/models/error_code.py
new file mode 100644
index 0000000..9e96e55
--- /dev/null
+++ b/faim_client/models/error_code.py
@@ -0,0 +1,34 @@
+from enum import Enum
+
+
+class ErrorCode(str, Enum):
+ AUTHENTICATION_FAILED = "AUTHENTICATION_FAILED"
+ AUTHENTICATION_REQUIRED = "AUTHENTICATION_REQUIRED"
+ AUTHORIZATION_FAILED = "AUTHORIZATION_FAILED"
+ BILLING_TRANSACTION_FAILED = "BILLING_TRANSACTION_FAILED"
+ CONFIGURATION_ERROR = "CONFIGURATION_ERROR"
+ DATABASE_ERROR = "DATABASE_ERROR"
+ INFERENCE_ERROR = "INFERENCE_ERROR"
+ INSUFFICIENT_FUNDS = "INSUFFICIENT_FUNDS"
+ INTERNAL_SERVER_ERROR = "INTERNAL_SERVER_ERROR"
+ INVALID_API_KEY = "INVALID_API_KEY"
+ INVALID_DTYPE = "INVALID_DTYPE"
+ INVALID_MODEL_INPUT = "INVALID_MODEL_INPUT"
+ INVALID_PARAMETER = "INVALID_PARAMETER"
+ INVALID_SHAPE = "INVALID_SHAPE"
+ INVALID_VALUE_RANGE = "INVALID_VALUE_RANGE"
+ MISSING_REQUIRED_FIELD = "MISSING_REQUIRED_FIELD"
+ MODEL_INITIALIZATION_ERROR = "MODEL_INITIALIZATION_ERROR"
+ MODEL_NOT_FOUND = "MODEL_NOT_FOUND"
+ OUT_OF_MEMORY = "OUT_OF_MEMORY"
+ PRICING_NOT_FOUND = "PRICING_NOT_FOUND"
+ RATE_LIMIT_EXCEEDED = "RATE_LIMIT_EXCEEDED"
+ REQUEST_TOO_LARGE = "REQUEST_TOO_LARGE"
+ RESOURCE_EXHAUSTED = "RESOURCE_EXHAUSTED"
+ RESOURCE_NOT_FOUND = "RESOURCE_NOT_FOUND"
+ TIMEOUT_ERROR = "TIMEOUT_ERROR"
+ TRITON_CONNECTION_ERROR = "TRITON_CONNECTION_ERROR"
+ VALIDATION_ERROR = "VALIDATION_ERROR"
+
+ def __str__(self) -> str:
+ return str(self.value)
diff --git a/faim_client/models/error_response.py b/faim_client/models/error_response.py
index 1848b05..070cfb5 100644
--- a/faim_client/models/error_response.py
+++ b/faim_client/models/error_response.py
@@ -4,6 +4,7 @@
from attrs import define as _attrs_define
from attrs import field as _attrs_field
+from ..models.error_code import ErrorCode
from ..types import UNSET, Unset
T = TypeVar("T", bound="ErrorResponse")
@@ -11,22 +12,39 @@
@_attrs_define
class ErrorResponse:
- """Error response model for API errors.
+ """Canonical error response structure for the entire FAIM system.
- Example:
- {'detail': "Model 'unknown_model' version '1' is not available", 'error': 'Model not found'}
+ This model is used by:
+ 1. Triton Python backends (serialized to JSON string in error metadata)
+ 2. Proxy Server (returned as HTTP JSON response body)
+ 3. Client SDK (deserialized from HTTP response)
Attributes:
- error (str): Error message
- detail (Union[None, Unset, str]): Additional error details
+ error_code: Machine-readable error code for programmatic handling
+ message: Human-readable error message
+ detail: Optional detailed explanation (backward compatible with SDK)
+ request_id: Request identifier for tracing
+
+ Attributes:
+ error_code (ErrorCode): Canonical error codes used across the entire system.
+
+ These codes are stable identifiers that clients can use for
+ programmatic error handling (retries, fallbacks, user messaging).
+ message (str): Human-readable error message
+ detail (Union[None, Unset, str]): Detailed error explanation (backward compatible with SDK ErrorResponse.detail)
+ request_id (Union[None, Unset, str]): Request identifier for distributed tracing
"""
- error: str
+ error_code: ErrorCode
+ message: str
detail: None | Unset | str = UNSET
+ request_id: None | Unset | str = UNSET
additional_properties: dict[str, Any] = _attrs_field(init=False, factory=dict)
def to_dict(self) -> dict[str, Any]:
- error = self.error
+ error_code = self.error_code.value
+
+ message = self.message
detail: None | Unset | str
if isinstance(self.detail, Unset):
@@ -34,22 +52,33 @@ def to_dict(self) -> dict[str, Any]:
else:
detail = self.detail
+ request_id: None | Unset | str
+ if isinstance(self.request_id, Unset):
+ request_id = UNSET
+ else:
+ request_id = self.request_id
+
field_dict: dict[str, Any] = {}
field_dict.update(self.additional_properties)
field_dict.update(
{
- "error": error,
+ "error_code": error_code,
+ "message": message,
}
)
if detail is not UNSET:
field_dict["detail"] = detail
+ if request_id is not UNSET:
+ field_dict["request_id"] = request_id
return field_dict
@classmethod
def from_dict(cls: type[T], src_dict: Mapping[str, Any]) -> T:
d = dict(src_dict)
- error = d.pop("error")
+ error_code = ErrorCode(d.pop("error_code"))
+
+ message = d.pop("message")
def _parse_detail(data: object) -> None | Unset | str:
if data is None:
@@ -60,9 +89,20 @@ def _parse_detail(data: object) -> None | Unset | str:
detail = _parse_detail(d.pop("detail", UNSET))
+ def _parse_request_id(data: object) -> None | Unset | str:
+ if data is None:
+ return data
+ if isinstance(data, Unset):
+ return data
+ return cast(None | Unset | str, data)
+
+ request_id = _parse_request_id(d.pop("request_id", UNSET))
+
error_response = cls(
- error=error,
+ error_code=error_code,
+ message=message,
detail=detail,
+ request_id=request_id,
)
error_response.additional_properties = d
diff --git a/faim_client/models/http_validation_error.py b/faim_client/models/http_validation_error.py
new file mode 100644
index 0000000..a2c8fa2
--- /dev/null
+++ b/faim_client/models/http_validation_error.py
@@ -0,0 +1,75 @@
+from collections.abc import Mapping
+from typing import TYPE_CHECKING, Any, TypeVar
+
+from attrs import define as _attrs_define
+from attrs import field as _attrs_field
+
+from ..types import UNSET, Unset
+
+if TYPE_CHECKING:
+ from ..models.validation_error import ValidationError
+
+
+T = TypeVar("T", bound="HTTPValidationError")
+
+
+@_attrs_define
+class HTTPValidationError:
+ """
+ Attributes:
+ detail (Union[Unset, list['ValidationError']]):
+ """
+
+ detail: Unset | list["ValidationError"] = UNSET
+ additional_properties: dict[str, Any] = _attrs_field(init=False, factory=dict)
+
+ def to_dict(self) -> dict[str, Any]:
+ detail: Unset | list[dict[str, Any]] = UNSET
+ if not isinstance(self.detail, Unset):
+ detail = []
+ for detail_item_data in self.detail:
+ detail_item = detail_item_data.to_dict()
+ detail.append(detail_item)
+
+ field_dict: dict[str, Any] = {}
+ field_dict.update(self.additional_properties)
+ field_dict.update({})
+ if detail is not UNSET:
+ field_dict["detail"] = detail
+
+ return field_dict
+
+ @classmethod
+ def from_dict(cls: type[T], src_dict: Mapping[str, Any]) -> T:
+ from ..models.validation_error import ValidationError
+
+ d = dict(src_dict)
+ detail = []
+ _detail = d.pop("detail", UNSET)
+ for detail_item_data in _detail or []:
+ detail_item = ValidationError.from_dict(detail_item_data)
+
+ detail.append(detail_item)
+
+ http_validation_error = cls(
+ detail=detail,
+ )
+
+ http_validation_error.additional_properties = d
+ return http_validation_error
+
+ @property
+ def additional_keys(self) -> list[str]:
+ return list(self.additional_properties.keys())
+
+ def __getitem__(self, key: str) -> Any:
+ return self.additional_properties[key]
+
+ def __setitem__(self, key: str, value: Any) -> None:
+ self.additional_properties[key] = value
+
+ def __delitem__(self, key: str) -> None:
+ del self.additional_properties[key]
+
+ def __contains__(self, key: str) -> bool:
+ return key in self.additional_properties
diff --git a/faim_client/models/model_name.py b/faim_client/models/model_name.py
index 2ea44ad..cfe67d1 100644
--- a/faim_client/models/model_name.py
+++ b/faim_client/models/model_name.py
@@ -2,8 +2,9 @@
class ModelName(str, Enum):
+ CHRONOS2 = "chronos2"
FLOWSTATE = "flowstate"
- TOTO = "toto"
+ TIREX = "tirex"
def __str__(self) -> str:
return str(self.value)
diff --git a/faim_client/models/validation_error.py b/faim_client/models/validation_error.py
new file mode 100644
index 0000000..1d581e1
--- /dev/null
+++ b/faim_client/models/validation_error.py
@@ -0,0 +1,88 @@
+from collections.abc import Mapping
+from typing import Any, TypeVar, cast
+
+from attrs import define as _attrs_define
+from attrs import field as _attrs_field
+
+T = TypeVar("T", bound="ValidationError")
+
+
+@_attrs_define
+class ValidationError:
+ """
+ Attributes:
+ loc (list[Union[int, str]]):
+ msg (str):
+ type_ (str):
+ """
+
+ loc: list[int | str]
+ msg: str
+ type_: str
+ additional_properties: dict[str, Any] = _attrs_field(init=False, factory=dict)
+
+ def to_dict(self) -> dict[str, Any]:
+ loc = []
+ for loc_item_data in self.loc:
+ loc_item: int | str
+ loc_item = loc_item_data
+ loc.append(loc_item)
+
+ msg = self.msg
+
+ type_ = self.type_
+
+ field_dict: dict[str, Any] = {}
+ field_dict.update(self.additional_properties)
+ field_dict.update(
+ {
+ "loc": loc,
+ "msg": msg,
+ "type": type_,
+ }
+ )
+
+ return field_dict
+
+ @classmethod
+ def from_dict(cls: type[T], src_dict: Mapping[str, Any]) -> T:
+ d = dict(src_dict)
+ loc = []
+ _loc = d.pop("loc")
+ for loc_item_data in _loc:
+
+ def _parse_loc_item(data: object) -> int | str:
+ return cast(int | str, data)
+
+ loc_item = _parse_loc_item(loc_item_data)
+
+ loc.append(loc_item)
+
+ msg = d.pop("msg")
+
+ type_ = d.pop("type")
+
+ validation_error = cls(
+ loc=loc,
+ msg=msg,
+ type_=type_,
+ )
+
+ validation_error.additional_properties = d
+ return validation_error
+
+ @property
+ def additional_keys(self) -> list[str]:
+ return list(self.additional_properties.keys())
+
+ def __getitem__(self, key: str) -> Any:
+ return self.additional_properties[key]
+
+ def __setitem__(self, key: str, value: Any) -> None:
+ self.additional_properties[key] = value
+
+ def __delitem__(self, key: str) -> None:
+ del self.additional_properties[key]
+
+ def __contains__(self, key: str) -> bool:
+ return key in self.additional_properties
diff --git a/faim_sdk/__init__.py b/faim_sdk/__init__.py
index 062a6b2..cfa2614 100644
--- a/faim_sdk/__init__.py
+++ b/faim_sdk/__init__.py
@@ -2,27 +2,113 @@
This SDK provides a high-level, type-safe interface for interacting with the
FAIM inference platform for foundation AI models on structured data.
+
+Quick Start
+-----------
+
+Install the SDK:
+
+ pip install faim-sdk
+
+Basic usage:
+
+ >>> from faim_sdk import ForecastClient, Chronos2ForecastRequest
+ >>> import numpy as np
+ >>>
+ >>> # Initialize client
+ >>> client = ForecastClient(
+ ... base_url="https://api.faim.it.com",
+ ... api_key="your-api-key"
+ ... )
+ >>>
+ >>> # Create forecast request
+ >>> data = np.random.rand(32, 100, 1) # (batch_size, seq_len, features)
+ >>> request = Chronos2ForecastRequest(
+ ... x=data,
+ ... horizon=10,
+ ... output_type="quantiles",
+ ... quantiles=[0.1, 0.5, 0.9]
+ ... )
+ >>>
+ >>> # Generate forecast - model inferred automatically
+ >>> response = client.forecast(request)
+ >>> print(response.quantiles.shape) # (32, 10, 3)
+
+Main Components
+---------------
+
+ForecastClient:
+ High-level client for making forecast requests with automatic
+ serialization, error handling, and observability.
+
+Request Models:
+ - FlowStateForecastRequest: For FlowState model with scaling options
+ - Chronos2ForecastRequest: For Amazon Chronos 2.0 LLM-based forecasting
+ - TiRexForecastRequest: For TiRex transformer-based forecasting
+
+ForecastResponse:
+ Contains prediction outputs (point, quantiles, samples) and metadata.
+
+Evaluation Tools:
+ The faim_sdk.eval subpackage provides metrics and visualization:
+ - Metrics: mse, mase, crps_from_quantiles
+ - Visualization: plot_forecast (requires: pip install faim-sdk[viz])
+
+ >>> from faim_sdk.eval import mse, plot_forecast
+ >>> mse_score = mse(test_data, response.point)
+ >>> fig, ax = plot_forecast(train_data[0], response.point[0], test_data[0])
+
+Exceptions:
+ - AuthenticationError: API key issues (401, 403)
+ - ValidationError: Invalid request parameters (422)
+ - RateLimitError: Rate limit exceeded (429)
+ - ServiceUnavailableError: Temporary service issues (503, 504)
+ - And more... (see exceptions module for full hierarchy)
+
+Error Handling
+--------------
+
+The SDK uses machine-readable error codes for programmatic error handling:
+
+ >>> from faim_sdk import ValidationError, ErrorCode, Chronos2ForecastRequest
+ >>>
+ >>> try:
+ ... request = Chronos2ForecastRequest(x=data, horizon=10, quantiles=[0.1, 0.5, 0.9])
+ ... response = client.forecast(request)
+ >>> except ValidationError as e:
+ ... if e.error_code == ErrorCode.INVALID_SHAPE:
+ ... print(f"Shape error: {e.error_response.detail}")
+ ... print(f"Request ID: {e.error_response.request_id}")
+
+For more information, see the individual module documentation.
"""
+from faim_client.models.error_code import ErrorCode
+
from .client import ForecastClient
from .exceptions import (
APIError,
+ AuthenticationError,
ConfigurationError,
FAIMError,
+ InsufficientFundsError,
InternalServerError,
ModelNotFoundError,
NetworkError,
PayloadTooLargeError,
+ RateLimitError,
SerializationError,
+ ServiceUnavailableError,
TimeoutError,
ValidationError,
)
from .models import (
+ Chronos2ForecastRequest,
FlowStateForecastRequest,
ForecastRequest,
ForecastResponse,
OutputType,
- ToToForecastRequest,
+ TiRexForecastRequest,
)
__all__ = [
@@ -30,23 +116,30 @@
"ForecastClient",
# Request models
"ForecastRequest",
- "ToToForecastRequest",
"FlowStateForecastRequest",
+ "Chronos2ForecastRequest",
+ "TiRexForecastRequest",
# Response model
"ForecastResponse",
# Type aliases
"OutputType",
+ # Error codes (for programmatic error handling)
+ "ErrorCode",
# Exceptions
"FAIMError",
"APIError",
+ "AuthenticationError",
+ "InsufficientFundsError",
+ "RateLimitError",
"SerializationError",
"ModelNotFoundError",
"PayloadTooLargeError",
"ValidationError",
"InternalServerError",
+ "ServiceUnavailableError",
"NetworkError",
"TimeoutError",
"ConfigurationError",
]
-__version__ = "0.1.2"
+__version__ = "0.4.0"
diff --git a/faim_sdk/client.py b/faim_sdk/client.py
index aaf82cb..7562fd0 100644
--- a/faim_sdk/client.py
+++ b/faim_sdk/client.py
@@ -1,26 +1,35 @@
-""" FAIM SDK client for time-series forecasting.
+"""FAIM SDK client for time-series forecasting.
Provides high-level, type-safe API with automatic serialization, error handling,
and observability.
"""
import io
+import json
import logging
+import warnings
+from copy import copy
import httpx
+import numpy as np
from faim_client import AuthenticatedClient, Client
from faim_client.api.forecast import forecast_v1_ts_forecast_model_name_model_version_post
from faim_client.models import ModelName
+from faim_client.models.error_response import ErrorResponse
from faim_client.types import File
from .exceptions import (
APIError,
+ AuthenticationError,
+ InsufficientFundsError,
InternalServerError,
ModelNotFoundError,
NetworkError,
PayloadTooLargeError,
+ RateLimitError,
SerializationError,
+ ServiceUnavailableError,
TimeoutError,
ValidationError,
)
@@ -30,6 +39,169 @@
logger = logging.getLogger(__name__)
+def _parse_error_response(response) -> ErrorResponse | None:
+ """Parse ErrorResponse from HTTP response body.
+
+ Args:
+ response: HTTP response object from generated client
+
+ Returns:
+ Parsed ErrorResponse if available, None otherwise
+ """
+ try:
+ # Try parsing from response.parsed first (generated client parsing)
+ if hasattr(response, "parsed") and isinstance(response.parsed, ErrorResponse):
+ return response.parsed
+
+ # Fallback: try parsing JSON content directly
+ if hasattr(response, "content") and response.content:
+ error_dict = json.loads(response.content)
+ return ErrorResponse.from_dict(error_dict)
+ except Exception as e:
+ logger.warning(f"Failed to parse error response: {e}")
+
+ return None
+
+
+def _needs_univariate_transformation(request: ForecastRequest) -> bool:
+ """Check if request requires univariate transformation.
+
+ FlowState and TiRex models only support univariate forecasting.
+ When they receive multivariate input (features > 1), the input
+ must be transformed to forecast each feature independently.
+
+ Args:
+ request: Forecast request to check
+
+ Returns:
+ True if transformation is needed, False otherwise
+ """
+ # Only FlowState and TiRex require transformation
+ if request.model_name not in (ModelName.FLOWSTATE, ModelName.TIREX):
+ return False
+
+ # Check if input is multivariate (features > 1)
+ num_features = request.x.shape[2] # Shape is (batch, seq_len, features)
+ return num_features > 1
+
+
+def _prepare_univariate_request(request: ForecastRequest) -> tuple[ForecastRequest, tuple[int, int]]:
+ """Prepare request for univariate-only models with multivariate input.
+
+ Transforms input from (batch, seq_len, features) to (batch*features, seq_len, 1)
+ and issues a warning to the user that features will be forecast independently.
+
+ Args:
+ request: Original forecast request with multivariate input
+
+ Returns:
+ Tuple of (modified request, (original_batch_size, num_features))
+
+ Example:
+ Input shape: (batch=2, seq_len=100, features=3)
+ Output shape: (batch=6, seq_len=100, features=1)
+ Mapping:
+ - Feature 0 of series 0 โ new batch index 0
+ - Feature 1 of series 0 โ new batch index 1
+ - Feature 2 of series 0 โ new batch index 2
+ - Feature 0 of series 1 โ new batch index 3
+ - Feature 1 of series 1 โ new batch index 4
+ - Feature 2 of series 1 โ new batch index 5
+ """
+ original_batch_size, seq_len, num_features = request.x.shape
+
+ # Issue user warning
+ warnings.warn(
+ f"{request.model_name.value.title()} model only supports univariate forecasting. "
+ f"Input with {num_features} features will be forecast independently. "
+ f"Each feature will be treated as a separate time series.",
+ UserWarning,
+ stacklevel=3, # Point to the user's code, not this internal function
+ )
+
+ logger.info(
+ f"Transforming multivariate input for {request.model_name.value}: "
+ f"shape {request.x.shape} โ ({original_batch_size * num_features}, {seq_len}, 1)"
+ )
+
+ # Reshape: (batch, seq_len, features) โ (batch, features, seq_len) โ (batch*features, seq_len) โ (batch*features, seq_len, 1)
+ # We want to interleave features across the batch dimension
+ x_transposed = request.x.transpose(0, 2, 1) # (batch, features, seq_len)
+ x_flattened = x_transposed.reshape(original_batch_size * num_features, seq_len) # (batch*features, seq_len)
+ x_univariate = x_flattened[:, :, np.newaxis] # (batch*features, seq_len, 1)
+
+ # Create modified request with reshaped x
+ # Use copy to avoid modifying the original request
+ modified_request = copy(request)
+ modified_request.x = x_univariate
+
+ return modified_request, (original_batch_size, num_features)
+
+
+def _reshape_univariate_response(
+ response: ForecastResponse,
+ original_batch_size: int,
+ num_features: int,
+) -> ForecastResponse:
+ """Reshape response from univariate transformation back to multivariate format.
+
+ Reverses the transformation done by _prepare_univariate_request() to restore
+ the original batch and feature dimensions.
+
+ Args:
+ response: Response from server with univariate format
+ original_batch_size: Original batch size before transformation
+ num_features: Number of features in original input
+
+ Returns:
+ Response with proper multivariate shape
+
+ Example:
+ Point forecast:
+ Input shape: (batch*features=6, horizon=24, features=1)
+ Output shape: (batch=2, horizon=24, features=3)
+
+ Quantile forecast:
+ Input shape: (batch*features=6, horizon=24, quantiles=5, features=1)
+ Output shape: (batch=2, horizon=24, quantiles=5, features=3)
+ """
+ modified_response = ForecastResponse(metadata=response.metadata)
+
+ # Reshape point predictions if present
+ if response.point is not None:
+ # Input: (batch*features, horizon, 1)
+ # Output: (batch, horizon, features)
+ batch_times_features, horizon, _ = response.point.shape
+
+ # Reshape to (batch, features, horizon, 1)
+ reshaped = response.point.reshape(original_batch_size, num_features, horizon, 1)
+ # Transpose to (batch, horizon, features, 1)
+ transposed = reshaped.transpose(0, 2, 1, 3) # (batch, horizon, features, 1)
+ # Squeeze last dimension to get (batch, horizon, features)
+ modified_response.point = transposed.squeeze(-1)
+
+ # Reshape quantile predictions if present
+ if response.quantiles is not None:
+ # Input: (batch*features, horizon, quantiles, 1)
+ # Output: (batch, horizon, quantiles, features)
+ batch_times_features, horizon, num_quantiles, _ = response.quantiles.shape
+
+ # Reshape to (batch, features, horizon, quantiles, 1)
+ reshaped = response.quantiles.reshape(original_batch_size, num_features, horizon, num_quantiles, 1)
+ # Transpose to (batch, horizon, quantiles, features, 1)
+ transposed = reshaped.transpose(0, 2, 3, 1, 4) # (batch, horizon, quantiles, features, 1)
+ # Squeeze last dimension to get (batch, horizon, quantiles, features)
+ modified_response.quantiles = transposed.squeeze(-1)
+
+ # Samples - keep as is for now (not common for FlowState/TiRex)
+ if response.samples is not None:
+ modified_response.samples = response.samples
+
+ logger.debug(f"Reshaped univariate response: original_batch={original_batch_size}, features={num_features}")
+
+ return modified_response
+
+
class ForecastClient:
"""High-level client for FAIM time-series forecasting.
@@ -38,25 +210,25 @@ class ForecastClient:
- Comprehensive error handling with specific exception types
- Request/response logging for observability
- Support for both sync and async operations
+ - Automatic model inference from request type
Example:
- >>> from faim_sdk import ForecastClient, ToToForecastRequest
- >>> from faim_client.models import ModelName
+ >>> from faim_sdk import ForecastClient, Chronos2ForecastRequest
>>>
- >>> client = ForecastClient(base_url="https://api.example.com")
- >>> request = ToToForecastRequest(
+ >>> client = ForecastClient(base_url="https://api.faim.it.com")
+ >>> request = Chronos2ForecastRequest(
... x=data,
... horizon=10,
... quantiles=[0.1, 0.5, 0.9]
... )
- >>> response = client.forecast(ModelName.TOTO, request)
- >>> print(response.predictions.shape)
+ >>> response = client.forecast(request) # Model inferred automatically
+ >>> print(response.quantiles.shape)
"""
def __init__(
self,
- base_url: str,
- timeout: float = 120.0,
+ base_url: str = "https://api.faim.it.com",
+ timeout: float = 60.0,
verify_ssl: bool = True,
api_key: str | None = None,
**httpx_kwargs,
@@ -65,7 +237,7 @@ def __init__(
Args:
base_url: Base URL of FAIM inference API
- timeout: Request timeout in seconds. Default: 120s
+ timeout: Request timeout in seconds. Default: 60s
verify_ssl: Whether to verify SSL certificates. Default: True
api_key: Optional API key for authentication. If provided, all requests
will include "Authorization: Bearer " header. Default: None
@@ -104,35 +276,50 @@ def __init__(
)
logger.info(f"Initialized ForecastClient: base_url={base_url}, timeout={timeout}s")
- def forecast(self, model: ModelName, request: ForecastRequest) -> ForecastResponse:
+ def forecast(self, request: ForecastRequest) -> ForecastResponse:
"""Generate time-series forecast (synchronous).
+ The model is automatically inferred from the request type:
+ - FlowStateForecastRequest โ FlowState
+ - Chronos2ForecastRequest โ Chronos2
+ - TiRexForecastRequest โ TiRex
+
Args:
- model: Model to use (ModelName.TOTO or ModelName.FLOWSTATE)
- request: Model-specific forecast request
+ request: Model-specific forecast request (FlowStateForecastRequest,
+ Chronos2ForecastRequest, or TiRexForecastRequest)
Returns:
ForecastResponse with predictions and metadata
Raises:
- SerializationError: If request serialization or response deserialization fails
+ AuthenticationError: If authentication fails (401, 403)
+ InsufficientFundsError: If billing account balance is insufficient (402)
ModelNotFoundError: If model or version doesn't exist (404)
PayloadTooLargeError: If request exceeds size limit (413)
ValidationError: If request parameters are invalid (422)
+ RateLimitError: If rate limit exceeded (429)
InternalServerError: If backend encounters error (500)
+ ServiceUnavailableError: If service unavailable (503, 504)
NetworkError: If network communication fails
+ SerializationError: If request serialization or response deserialization fails
TimeoutError: If request exceeds timeout
APIError: For other API errors
Example:
>>> request = FlowStateForecastRequest(x=data, horizon=10)
- >>> response = client.forecast(ModelName.FLOWSTATE, request)
+ >>> response = client.forecast(request)
"""
+ model = request.model_name
logger.debug(
f"Starting forecast: model={model}, version={request.model_version}, "
f"x.shape={request.x.shape}, horizon={request.horizon}"
)
+ # Check if univariate transformation is needed
+ transform_shape_info = None
+ if _needs_univariate_transformation(request):
+ request, transform_shape_info = _prepare_univariate_request(request)
+
# Serialize request to Arrow format
try:
arrays, metadata = request.to_arrays_and_metadata()
@@ -187,46 +374,82 @@ def forecast(self, model: ModelName, request: ForecastRequest) -> ForecastRespon
details={"model": str(model), "error_type": type(e).__name__},
) from e
- # Handle HTTP errors
- if response.status_code == 404:
- logger.error(f"Model not found: {model}/{request.model_version}")
- raise ModelNotFoundError(
- f"Model {model}/{request.model_version} not found",
- status_code=404,
- response=response.parsed if hasattr(response, "parsed") else None,
- )
-
- elif response.status_code == 413:
- logger.error(f"Payload too large: {len(payload)} bytes")
- raise PayloadTooLargeError(
- f"Request payload too large ({len(payload)} bytes)",
- status_code=413,
- details={"payload_size": len(payload)},
- )
-
- elif response.status_code == 422:
- logger.error("Request validation failed")
- raise ValidationError(
- "Request parameters are invalid",
- status_code=422,
- response=response.parsed if hasattr(response, "parsed") else None,
- )
-
- elif response.status_code == 500:
- logger.error("Internal server error")
- raise InternalServerError(
- "Backend encountered an internal error",
- status_code=500,
- response=response.parsed if hasattr(response, "parsed") else None,
- )
-
- elif response.status_code != 200:
- logger.error(f"Unexpected status code: {response.status_code}")
- raise APIError(
- f"Unexpected status code: {response.status_code}",
- status_code=response.status_code,
- response=response.parsed if hasattr(response, "parsed") else None,
- )
+ # Handle non-200 responses with error contract
+ if response.status_code != 200:
+ error_response = _parse_error_response(response)
+
+ # Build error message from ErrorResponse or fallback
+ if error_response:
+ message = error_response.message
+ if error_response.detail:
+ message = f"{message}: {error_response.detail}"
+
+ logger.error(
+ f"API error: status={response.status_code}, "
+ f"code={error_response.error_code}, "
+ f"message={error_response.message}, "
+ f"request_id={error_response.request_id}"
+ )
+ else:
+ message = f"Request failed with status {response.status_code}"
+ logger.error(f"API error: status={response.status_code}, no error_response")
+
+ # Map HTTP status code to exception class
+ if response.status_code in (401, 403):
+ raise AuthenticationError(
+ message=message,
+ status_code=response.status_code,
+ error_response=error_response,
+ )
+ elif response.status_code == 402:
+ raise InsufficientFundsError(
+ message=message,
+ status_code=response.status_code,
+ error_response=error_response,
+ )
+ elif response.status_code == 404:
+ raise ModelNotFoundError(
+ message=message,
+ status_code=response.status_code,
+ error_response=error_response,
+ )
+ elif response.status_code == 413:
+ raise PayloadTooLargeError(
+ message=message,
+ status_code=response.status_code,
+ error_response=error_response,
+ )
+ elif response.status_code == 422:
+ raise ValidationError(
+ message=message,
+ status_code=response.status_code,
+ error_response=error_response,
+ )
+ elif response.status_code == 429:
+ raise RateLimitError(
+ message=message,
+ status_code=response.status_code,
+ error_response=error_response,
+ )
+ elif response.status_code == 500:
+ raise InternalServerError(
+ message=message,
+ status_code=response.status_code,
+ error_response=error_response,
+ )
+ elif response.status_code in (503, 504):
+ raise ServiceUnavailableError(
+ message=message,
+ status_code=response.status_code,
+ error_response=error_response,
+ )
+ else:
+ # Fallback for unmapped status codes
+ raise APIError(
+ message=message,
+ status_code=response.status_code,
+ error_response=error_response,
+ )
# Deserialize successful response
try:
@@ -236,6 +459,11 @@ def forecast(self, model: ModelName, request: ForecastRequest) -> ForecastRespon
arrays, metadata = deserialize_from_arrow(response_bytes)
forecast_response = ForecastResponse.from_arrays_and_metadata(arrays, metadata)
+ # If univariate transformation was applied, reshape response back
+ if transform_shape_info is not None:
+ original_batch_size, num_features = transform_shape_info
+ forecast_response = _reshape_univariate_response(forecast_response, original_batch_size, num_features)
+
logger.info(f"Forecast successful: {forecast_response}")
return forecast_response
@@ -246,12 +474,17 @@ def forecast(self, model: ModelName, request: ForecastRequest) -> ForecastRespon
details={"model": str(model), "error": str(e)},
) from e
- async def forecast_async(self, model: ModelName, request: ForecastRequest) -> ForecastResponse:
+ async def forecast_async(self, request: ForecastRequest) -> ForecastResponse:
"""Generate time-series forecast (asynchronous).
+ The model is automatically inferred from the request type:
+ - FlowStateForecastRequest โ FlowState
+ - Chronos2ForecastRequest โ Chronos2
+ - TiRexForecastRequest โ TiRex
+
Args:
- model: Model to use (ModelName.TOTO or ModelName.FLOWSTATE)
- request: Model-specific forecast request
+ request: Model-specific forecast request (FlowStateForecastRequest,
+ Chronos2ForecastRequest, or TiRexForecastRequest)
Returns:
ForecastResponse with predictions and metadata
@@ -260,11 +493,17 @@ async def forecast_async(self, model: ModelName, request: ForecastRequest) -> Fo
Same exceptions as forecast()
Example:
- >>> request = ToToForecastRequest(x=data, horizon=10)
- >>> response = await client.forecast_async(ModelName.TOTO, request)
+ >>> request = Chronos2ForecastRequest(x=data, horizon=10)
+ >>> response = await client.forecast_async(request)
"""
+ model = request.model_name
logger.debug(f"Starting async forecast: model={model}, version={request.model_version}")
+ # Check if univariate transformation is needed
+ transform_shape_info = None
+ if _needs_univariate_transformation(request):
+ request, transform_shape_info = _prepare_univariate_request(request)
+
# Serialize request
try:
arrays, metadata = request.to_arrays_and_metadata()
@@ -319,37 +558,82 @@ async def forecast_async(self, model: ModelName, request: ForecastRequest) -> Fo
details={"model": str(model), "error_type": type(e).__name__},
) from e
- # Handle HTTP errors (same as sync)
- if response.status_code == 404:
- raise ModelNotFoundError(
- f"Model {model}/{request.model_version} not found",
- status_code=404,
- response=response.parsed if hasattr(response, "parsed") else None,
- )
- elif response.status_code == 413:
- raise PayloadTooLargeError(
- f"Request payload too large ({len(payload)} bytes)",
- status_code=413,
- details={"payload_size": len(payload)},
- )
- elif response.status_code == 422:
- raise ValidationError(
- "Request parameters are invalid",
- status_code=422,
- response=response.parsed if hasattr(response, "parsed") else None,
- )
- elif response.status_code == 500:
- raise InternalServerError(
- "Backend encountered an internal error",
- status_code=500,
- response=response.parsed if hasattr(response, "parsed") else None,
- )
- elif response.status_code != 200:
- raise APIError(
- f"Unexpected status code: {response.status_code}",
- status_code=response.status_code,
- response=response.parsed if hasattr(response, "parsed") else None,
- )
+ # Handle non-200 responses with error contract (same as sync)
+ if response.status_code != 200:
+ error_response = _parse_error_response(response)
+
+ # Build error message from ErrorResponse or fallback
+ if error_response:
+ message = error_response.message
+ if error_response.detail:
+ message = f"{message}: {error_response.detail}"
+
+ logger.error(
+ f"API error: status={response.status_code}, "
+ f"code={error_response.error_code}, "
+ f"message={error_response.message}, "
+ f"request_id={error_response.request_id}"
+ )
+ else:
+ message = f"Request failed with status {response.status_code}"
+ logger.error(f"API error: status={response.status_code}, no error_response")
+
+ # Map HTTP status code to exception class
+ if response.status_code in (401, 403):
+ raise AuthenticationError(
+ message=message,
+ status_code=response.status_code,
+ error_response=error_response,
+ )
+ elif response.status_code == 402:
+ raise InsufficientFundsError(
+ message=message,
+ status_code=response.status_code,
+ error_response=error_response,
+ )
+ elif response.status_code == 404:
+ raise ModelNotFoundError(
+ message=message,
+ status_code=response.status_code,
+ error_response=error_response,
+ )
+ elif response.status_code == 413:
+ raise PayloadTooLargeError(
+ message=message,
+ status_code=response.status_code,
+ error_response=error_response,
+ )
+ elif response.status_code == 422:
+ raise ValidationError(
+ message=message,
+ status_code=response.status_code,
+ error_response=error_response,
+ )
+ elif response.status_code == 429:
+ raise RateLimitError(
+ message=message,
+ status_code=response.status_code,
+ error_response=error_response,
+ )
+ elif response.status_code == 500:
+ raise InternalServerError(
+ message=message,
+ status_code=response.status_code,
+ error_response=error_response,
+ )
+ elif response.status_code in (503, 504):
+ raise ServiceUnavailableError(
+ message=message,
+ status_code=response.status_code,
+ error_response=error_response,
+ )
+ else:
+ # Fallback for unmapped status codes
+ raise APIError(
+ message=message,
+ status_code=response.status_code,
+ error_response=error_response,
+ )
# Deserialize response
try:
@@ -359,6 +643,11 @@ async def forecast_async(self, model: ModelName, request: ForecastRequest) -> Fo
arrays, metadata = deserialize_from_arrow(response_bytes)
forecast_response = ForecastResponse.from_arrays_and_metadata(arrays, metadata)
+ # If univariate transformation was applied, reshape response back
+ if transform_shape_info is not None:
+ original_batch_size, num_features = transform_shape_info
+ forecast_response = _reshape_univariate_response(forecast_response, original_batch_size, num_features)
+
logger.info(f"Async forecast successful: {forecast_response}")
return forecast_response
@@ -370,29 +659,93 @@ async def forecast_async(self, model: ModelName, request: ForecastRequest) -> Fo
) from e
def close(self) -> None:
- """Close underlying HTTP client and release resources."""
+ """Close underlying HTTP client and release resources.
+
+ Should be called when the client is no longer needed to properly
+ release connection pool resources. Alternatively, use the client
+ as a context manager which handles cleanup automatically.
+
+ Example:
+ >>> client = ForecastClient(base_url="https://api.example.com")
+ >>> try:
+ ... response = client.forecast(model, request)
+ ... finally:
+ ... client.close()
+ """
if hasattr(self._client, "_client") and self._client._client:
self._client._client.close()
logger.debug("ForecastClient closed")
async def aclose(self) -> None:
- """Close underlying async HTTP client and release resources."""
+ """Close underlying async HTTP client and release resources.
+
+ Async equivalent of close(). Should be called when the async client
+ is no longer needed. Alternatively, use the client as an async
+ context manager which handles cleanup automatically.
+
+ Example:
+ >>> client = ForecastClient(base_url="https://api.example.com")
+ >>> try:
+ ... response = await client.forecast_async(model, request)
+ ... finally:
+ ... await client.aclose()
+ """
if hasattr(self._client, "_async_client") and self._client._async_client:
await self._client._async_client.aclose()
logger.debug("Async ForecastClient closed")
def __enter__(self) -> "ForecastClient":
- """Enter sync context manager."""
+ """Enter sync context manager.
+
+ Enables using the client with Python's 'with' statement for
+ automatic resource cleanup.
+
+ Returns:
+ The client instance
+
+ Example:
+ >>> with ForecastClient(base_url="https://api.example.com") as client:
+ ... response = client.forecast(request)
+ ... # Client automatically closed on exit
+ """
return self
def __exit__(self, *args) -> None:
- """Exit sync context manager."""
+ """Exit sync context manager and release resources.
+
+ Automatically called when exiting a 'with' block. Ensures proper
+ cleanup of HTTP connections.
+
+ Args:
+ *args: Exception information (exc_type, exc_value, traceback) if an
+ exception occurred within the with block
+ """
self.close()
async def __aenter__(self) -> "ForecastClient":
- """Enter async context manager."""
+ """Enter async context manager.
+
+ Enables using the client with Python's 'async with' statement for
+ automatic resource cleanup in async code.
+
+ Returns:
+ The client instance
+
+ Example:
+ >>> async with ForecastClient(base_url="https://api.example.com") as client:
+ ... response = await client.forecast_async(request)
+ ... # Client automatically closed on exit
+ """
return self
async def __aexit__(self, *args) -> None:
- """Exit async context manager."""
+ """Exit async context manager and release resources.
+
+ Automatically called when exiting an 'async with' block. Ensures proper
+ cleanup of HTTP connections.
+
+ Args:
+ *args: Exception information (exc_type, exc_value, traceback) if an
+ exception occurred within the async with block
+ """
await self.aclose()
diff --git a/faim_sdk/eval/__init__.py b/faim_sdk/eval/__init__.py
new file mode 100644
index 0000000..02db051
--- /dev/null
+++ b/faim_sdk/eval/__init__.py
@@ -0,0 +1,78 @@
+"""Evaluation tools for time-series forecasting.
+
+This subpackage provides metrics and visualization utilities for evaluating
+forecast quality. All functions work seamlessly with FAIM SDK responses.
+
+Modules:
+ - metrics: MSE, MASE, and CRPS metrics for forecast evaluation
+ - visualization: plot_forecast for visualizing forecasts
+
+Quick Start - Metrics:
+ >>> from faim_sdk import ForecastClient, Chronos2ForecastRequest
+ >>> from faim_sdk.eval import mse, mase, crps_from_quantiles
+ >>> from faim_client.models import ModelName
+ >>> import numpy as np
+ >>>
+ >>> # Generate forecast
+ >>> client = ForecastClient()
+ >>> request = Chronos2ForecastRequest(
+ ... x=train_data, # (32, 100, 1)
+ ... horizon=24,
+ ... output_type="quantiles",
+ ... quantiles=[0.1, 0.5, 0.9]
+ ... )
+ >>> response = client.forecast(ModelName.CHRONOS2, request)
+ >>>
+ >>> # Evaluate point forecast (use median)
+ >>> point_pred = response.quantiles[:, :, 1:2] # Keep 3D shape
+ >>> mse_score = mse(test_data, point_pred)
+ >>> mase_score = mase(test_data, point_pred, train_data)
+ >>>
+ >>> # Evaluate probabilistic forecast
+ >>> crps_score = crps_from_quantiles(
+ ... test_data,
+ ... response.quantiles,
+ ... quantile_levels=[0.1, 0.5, 0.9]
+ ... )
+
+Quick Start - Visualization:
+ >>> from faim_sdk.eval import plot_forecast
+ >>>
+ >>> # Plot single sample (remember to index batch dimension!)
+ >>> fig, ax = plot_forecast(
+ ... train_data=train_data[0], # (100, 1) - 2D array
+ ... forecast=response.point[0], # (24, 1) - 2D array
+ ... test_data=test_data[0], # (24, 1) - 2D array
+ ... title="Forecast Visualization"
+ ... )
+ >>> fig.savefig("forecast.png")
+
+Available Metrics:
+ - mse: Mean Squared Error (point forecasts)
+ - mae: Mean Absolute Error (point forecasts)
+ - mase: Mean Absolute Scaled Error (scale-independent)
+ - crps_from_quantiles: Continuous Ranked Probability Score (probabilistic)
+
+Available Visualization:
+ - plot_forecast: Plot training data, forecast, and optional test data
+
+Installation:
+ For visualization support, install with the viz extra:
+ pip install faim-sdk[viz]
+
+ Or install matplotlib separately:
+ pip install matplotlib
+"""
+
+from .metrics import crps_from_quantiles, mae, mase, mse
+from .visualization import plot_forecast
+
+__all__ = [
+ # Metrics
+ "mse",
+ "mae",
+ "mase",
+ "crps_from_quantiles",
+ # Visualization
+ "plot_forecast",
+]
diff --git a/faim_sdk/eval/metrics.py b/faim_sdk/eval/metrics.py
new file mode 100644
index 0000000..833f6a1
--- /dev/null
+++ b/faim_sdk/eval/metrics.py
@@ -0,0 +1,628 @@
+"""Evaluation metrics for time-series forecasting.
+
+This module provides production-ready implementations of standard forecasting
+evaluation metrics. All functions support batch processing and flexible
+aggregation options.
+
+All metric functions expect 3D input arrays with shape:
+ (batch_size, horizon, features)
+
+Typical Usage:
+ >>> from faim_sdk import ForecastClient, Chronos2ForecastRequest
+ >>> from faim_sdk.eval import mse, mase, crps_from_quantiles
+ >>> from faim_client.models import ModelName
+ >>> import numpy as np
+ >>>
+ >>> # Generate forecast
+ >>> client = ForecastClient()
+ >>> request = Chronos2ForecastRequest(
+ ... x=train_data, # shape: (32, 100, 1)
+ ... horizon=24,
+ ... output_type="quantiles",
+ ... quantiles=[0.1, 0.5, 0.9]
+ ... )
+ >>> response = client.forecast(ModelName.CHRONOS2, request)
+ >>>
+ >>> # Evaluate point forecasts (use median as point forecast)
+ >>> point_pred = response.quantiles[:, :, 1:2] # (32, 24, 1) - keep 3D shape
+ >>> mse_score = mse(test_data, point_pred, reduction='mean')
+ >>> mase_score = mase(test_data, point_pred, train_data, reduction='mean')
+ >>>
+ >>> # Evaluate probabilistic forecasts
+ >>> crps_score = crps_from_quantiles(
+ ... test_data,
+ ... response.quantiles,
+ ... quantile_levels=[0.1, 0.5, 0.9],
+ ... reduction='mean'
+ ... )
+
+Available Metrics:
+ - mse: Mean Squared Error (point forecasts)
+ - mase: Mean Absolute Scaled Error (point forecasts, scale-independent)
+ - crps_from_quantiles: Continuous Ranked Probability Score (probabilistic forecasts)
+"""
+
+from typing import Literal
+
+import numpy as np
+
+ReductionType = Literal["mean", "none"]
+
+
+def mse(
+ y_true: np.ndarray,
+ y_pred: np.ndarray,
+ reduction: ReductionType = "mean",
+) -> float | np.ndarray:
+ """Calculate Mean Squared Error between true and predicted values.
+
+ MSE measures the average squared difference between predictions and ground truth.
+ Lower values indicate better forecast accuracy.
+
+ Mathematical Formula:
+ MSE = (1/n) * ฮฃ(y_true - y_pred)ยฒ
+
+ where n is the total number of predictions (batch_size ร horizon ร features).
+
+ Args:
+ y_true: Ground truth values. Shape: (batch_size, horizon, features).
+ y_pred: Predicted values. Shape: (batch_size, horizon, features).
+ reduction: How to aggregate results across the batch dimension.
+ - 'mean': Return single scalar averaged across all dimensions (default)
+ - 'none': Return per-sample metrics with shape (batch_size,)
+
+ Returns:
+ MSE value(s). Returns float if reduction='mean', otherwise array of shape
+ (batch_size,) containing MSE for each sample in the batch.
+
+ Raises:
+ TypeError: If inputs are not numpy arrays.
+ ValueError: If inputs have different shapes, wrong number of dimensions,
+ or are empty.
+
+ Examples:
+ >>> import numpy as np
+ >>> from faim_sdk.eval import mse
+ >>>
+ >>> # Single feature, batch of 4 samples
+ >>> y_true = np.array([[[1.0], [2.0], [3.0]],
+ ... [[4.0], [5.0], [6.0]],
+ ... [[7.0], [8.0], [9.0]],
+ ... [[10.0], [11.0], [12.0]]]) # (4, 3, 1)
+ >>> y_pred = np.array([[[1.1], [2.1], [3.1]],
+ ... [[4.1], [5.1], [6.1]],
+ ... [[7.1], [8.1], [9.1]],
+ ... [[10.1], [11.1], [12.1]]]) # (4, 3, 1)
+ >>>
+ >>> # Overall MSE
+ >>> mse(y_true, y_pred, reduction='mean')
+ 0.010000000000000002
+ >>>
+ >>> # Per-sample MSE
+ >>> mse(y_true, y_pred, reduction='none')
+ array([0.01, 0.01, 0.01, 0.01])
+ >>>
+ >>> # Multi-feature example
+ >>> y_true = np.random.randn(32, 24, 5) # 32 samples, 24 steps, 5 features
+ >>> y_pred = y_true + np.random.randn(32, 24, 5) * 0.1 # Add small noise
+ >>> mse_score = mse(y_true, y_pred) # Returns scalar
+ >>> print(f"MSE: {mse_score:.4f}")
+
+ Notes:
+ - MSE is sensitive to outliers due to squaring errors
+ - MSE units are squared units of the original data
+ - MSE is always non-negative (0 = perfect forecast)
+ - For scale-independent evaluation, consider using MASE instead
+ """
+ # Type validation
+ if not isinstance(y_true, np.ndarray):
+ raise TypeError(f"y_true must be numpy.ndarray, got {type(y_true).__name__}")
+ if not isinstance(y_pred, np.ndarray):
+ raise TypeError(f"y_pred must be numpy.ndarray, got {type(y_pred).__name__}")
+
+ # Shape validation
+ if y_true.ndim != 3:
+ raise ValueError(f"y_true must be 3-dimensional (batch_size, horizon, features), got shape {y_true.shape}")
+ if y_pred.ndim != 3:
+ raise ValueError(f"y_pred must be 3-dimensional (batch_size, horizon, features), got shape {y_pred.shape}")
+ if y_true.shape != y_pred.shape:
+ raise ValueError(f"y_true and y_pred must have the same shape, got {y_true.shape} and {y_pred.shape}")
+
+ # Empty validation
+ if y_true.size == 0:
+ raise ValueError("y_true cannot be empty")
+
+ # Calculate squared errors
+ squared_errors = (y_true - y_pred) ** 2
+
+ # Apply reduction
+ if reduction == "mean":
+ return float(np.mean(squared_errors))
+ elif reduction == "none":
+ # Average over horizon and features, keep batch dimension
+ return np.mean(squared_errors, axis=(1, 2))
+ else:
+ raise ValueError(f"reduction must be 'mean' or 'none', got '{reduction}'")
+
+
+def mae(
+ y_true: np.ndarray,
+ y_pred: np.ndarray,
+ reduction: ReductionType = "mean",
+) -> float | np.ndarray:
+ """Calculate Mean Absolute Error between true and predicted values.
+
+ MAE measures the average absolute difference between predictions and ground truth.
+ Lower values indicate better forecast accuracy. Unlike MSE, MAE is less sensitive
+ to outliers and maintains the same units as the original data.
+
+ Mathematical Formula:
+ MAE = (1/n) * ฮฃ|y_true - y_pred|
+
+ where n is the total number of predictions (batch_size ร horizon ร features).
+
+ Args:
+ y_true: Ground truth values. Shape: (batch_size, horizon, features).
+ y_pred: Predicted values. Shape: (batch_size, horizon, features).
+ reduction: How to aggregate results across the batch dimension.
+ - 'mean': Return single scalar averaged across all dimensions (default)
+ - 'none': Return per-sample metrics with shape (batch_size,)
+
+ Returns:
+ MAE value(s). Returns float if reduction='mean', otherwise array of shape
+ (batch_size,) containing MAE for each sample in the batch.
+
+ Raises:
+ TypeError: If inputs are not numpy arrays.
+ ValueError: If inputs have different shapes, wrong number of dimensions,
+ or are empty.
+
+ Examples:
+ >>> import numpy as np
+ >>> from faim_sdk.eval import mae
+ >>>
+ >>> # Single feature, batch of 4 samples
+ >>> y_true = np.array([[[1.0], [2.0], [3.0]],
+ ... [[4.0], [5.0], [6.0]]]) # (2, 3, 1)
+ >>> y_pred = np.array([[[1.1], [2.1], [3.1]],
+ ... [[4.2], [5.2], [6.2]]]) # (2, 3, 1)
+ >>>
+ >>> # Overall MAE
+ >>> mae(y_true, y_pred, reduction='mean')
+ 0.15
+ >>>
+ >>> # Per-sample MAE
+ >>> mae(y_true, y_pred, reduction='none')
+ array([0.1, 0.2])
+
+ Notes:
+ - MAE is less sensitive to outliers compared to MSE
+ - MAE has the same units as the original data
+ - MAE is always non-negative (0 = perfect forecast)
+ - For scale-independent evaluation, consider using MASE instead
+ """
+ # Type validation
+ if not isinstance(y_true, np.ndarray):
+ raise TypeError(f"y_true must be numpy.ndarray, got {type(y_true).__name__}")
+ if not isinstance(y_pred, np.ndarray):
+ raise TypeError(f"y_pred must be numpy.ndarray, got {type(y_pred).__name__}")
+
+ # Shape validation
+ if y_true.ndim != 3:
+ raise ValueError(f"y_true must be 3-dimensional (batch_size, horizon, features), got shape {y_true.shape}")
+ if y_pred.ndim != 3:
+ raise ValueError(f"y_pred must be 3-dimensional (batch_size, horizon, features), got shape {y_pred.shape}")
+ if y_true.shape != y_pred.shape:
+ raise ValueError(f"y_true and y_pred must have the same shape, got {y_true.shape} and {y_pred.shape}")
+
+ # Empty validation
+ if y_true.size == 0:
+ raise ValueError("y_true cannot be empty")
+
+ # Calculate absolute errors
+ absolute_errors = np.abs(y_true - y_pred)
+
+ # Apply reduction
+ if reduction == "mean":
+ return float(np.mean(absolute_errors))
+ elif reduction == "none":
+ # Average over horizon and features, keep batch dimension
+ return np.mean(absolute_errors, axis=(1, 2))
+ else:
+ raise ValueError(f"reduction must be 'mean' or 'none', got '{reduction}'")
+
+
+def mase(
+ y_true: np.ndarray,
+ y_pred: np.ndarray,
+ y_train: np.ndarray,
+ reduction: ReductionType = "mean",
+) -> float | np.ndarray:
+ """Calculate Mean Absolute Scaled Error for forecast evaluation.
+
+ MASE is a scale-independent metric that measures forecast accuracy relative
+ to a naive baseline forecast. It normalizes the forecast error by the average
+ error of a naive forecast on the training data.
+
+ The naive baseline used here is the "last value" forecast: predicting each
+ training value as the previous training value.
+
+ Mathematical Formula:
+ MASE = MAE(forecast) / MAE(naive baseline on training data)
+
+ where:
+ - MAE(forecast) = (1/n) * ฮฃ|y_true - y_pred|
+ - MAE(naive baseline) = (1/(T-1)) * ฮฃ|y_train[t] - y_train[t-1]|
+
+ Interpretation:
+ - MASE < 1: Forecast is better than naive baseline
+ - MASE = 1: Forecast is equivalent to naive baseline
+ - MASE > 1: Forecast is worse than naive baseline
+
+ Args:
+ y_true: Ground truth values. Shape: (batch_size, horizon, features).
+ y_pred: Predicted values. Shape: (batch_size, horizon, features).
+ y_train: Historical training data used for baseline calculation.
+ Shape: (batch_size, train_length, features).
+ Should contain at least 2 time steps per sample.
+ reduction: How to aggregate results across the batch dimension.
+ - 'mean': Return single scalar averaged across batch (default)
+ - 'none': Return per-sample metrics with shape (batch_size,)
+
+ Returns:
+ MASE value(s). Returns float if reduction='mean', otherwise array of shape
+ (batch_size,) containing MASE for each sample in the batch.
+
+ Raises:
+ TypeError: If inputs are not numpy arrays.
+ ValueError: If inputs have incompatible shapes, wrong dimensions,
+ or training data is too short (< 2 time steps).
+ RuntimeWarning: If naive baseline MAE is zero (constant training series),
+ MASE will be infinite.
+
+ Examples:
+ >>> import numpy as np
+ >>> from faim_sdk.eval import mase
+ >>>
+ >>> # Example with trend
+ >>> y_train = np.array([[[1.0], [2.0], [3.0], [4.0], [5.0]]]) # (1, 5, 1)
+ >>> y_true = np.array([[[6.0], [7.0], [8.0]]]) # (1, 3, 1)
+ >>> y_pred = np.array([[[6.1], [7.1], [8.1]]]) # (1, 3, 1)
+ >>>
+ >>> # Naive baseline MAE = mean(|1-2|, |2-3|, |3-4|, |4-5|) = 1.0
+ >>> # Forecast MAE = mean(|6-6.1|, |7-7.1|, |8-8.1|) = 0.1
+ >>> # MASE = 0.1 / 1.0 = 0.1 (excellent, much better than naive)
+ >>> mase(y_true, y_pred, y_train)
+ 0.1
+ >>>
+ >>> # Batch example with multiple samples
+ >>> y_train = np.random.randn(32, 100, 5) # 32 samples, 100 train steps
+ >>> y_true = np.random.randn(32, 24, 5) # 24 forecast steps
+ >>> y_pred = np.random.randn(32, 24, 5)
+ >>>
+ >>> # Overall MASE
+ >>> mase_score = mase(y_true, y_pred, y_train, reduction='mean')
+ >>> print(f"MASE: {mase_score:.4f}")
+ >>>
+ >>> # Per-sample MASE
+ >>> mase_per_sample = mase(y_true, y_pred, y_train, reduction='none')
+ >>> print(f"MASE range: [{mase_per_sample.min():.2f}, {mase_per_sample.max():.2f}]")
+
+ Notes:
+ - MASE is scale-independent, allowing comparison across different series
+ - MASE is recommended by Hyndman & Koehler (2006) for forecast evaluation
+ - Unlike MAPE, MASE works well with zero or near-zero values
+ - If training data is constant (naive MAE = 0), MASE will be infinite
+ - For seasonal data, consider using seasonal naive baseline instead
+
+ References:
+ Hyndman, R. J., & Koehler, A. B. (2006). Another look at measures of
+ forecast accuracy. International Journal of Forecasting, 22(4), 679-688.
+ """
+ # Type validation
+ if not isinstance(y_true, np.ndarray):
+ raise TypeError(f"y_true must be numpy.ndarray, got {type(y_true).__name__}")
+ if not isinstance(y_pred, np.ndarray):
+ raise TypeError(f"y_pred must be numpy.ndarray, got {type(y_pred).__name__}")
+ if not isinstance(y_train, np.ndarray):
+ raise TypeError(f"y_train must be numpy.ndarray, got {type(y_train).__name__}")
+
+ # Shape validation
+ if y_true.ndim != 3:
+ raise ValueError(f"y_true must be 3-dimensional (batch_size, horizon, features), got shape {y_true.shape}")
+ if y_pred.ndim != 3:
+ raise ValueError(f"y_pred must be 3-dimensional (batch_size, horizon, features), got shape {y_pred.shape}")
+ if y_train.ndim != 3:
+ raise ValueError(
+ f"y_train must be 3-dimensional (batch_size, train_length, features), got shape {y_train.shape}"
+ )
+
+ if y_true.shape != y_pred.shape:
+ raise ValueError(f"y_true and y_pred must have the same shape, got {y_true.shape} and {y_pred.shape}")
+
+ batch_size_true, _, features_true = y_true.shape
+ batch_size_train, train_length, features_train = y_train.shape
+
+ if batch_size_true != batch_size_train:
+ raise ValueError(
+ f"Batch size mismatch: y_true has {batch_size_true} samples, y_train has {batch_size_train} samples"
+ )
+
+ if features_true != features_train:
+ raise ValueError(
+ f"Feature count mismatch: y_true has {features_true} features, y_train has {features_train} features"
+ )
+
+ # Training data length validation
+ if train_length < 2:
+ raise ValueError(f"y_train must have at least 2 time steps for naive baseline, got {train_length}")
+
+ # Empty validation
+ if y_true.size == 0:
+ raise ValueError("y_true cannot be empty")
+ if y_train.size == 0:
+ raise ValueError("y_train cannot be empty")
+
+ # Calculate forecast MAE
+ forecast_mae = np.abs(y_true - y_pred)
+
+ # Calculate naive baseline MAE (last value forecast on training data)
+ # Naive forecast: y_train[t] predicts y_train[t-1]
+ naive_errors = np.abs(y_train[:, 1:, :] - y_train[:, :-1, :])
+ naive_mae = np.mean(naive_errors, axis=1, keepdims=True) # Shape: (batch_size, 1, features)
+
+ # Warn if naive MAE is zero (constant series)
+ if np.any(naive_mae == 0):
+ import warnings
+
+ warnings.warn(
+ "Naive baseline MAE is zero for some samples (constant training series). "
+ "MASE will be infinite for these samples.",
+ RuntimeWarning,
+ stacklevel=2,
+ )
+
+ # Calculate MASE
+ # Avoid division by zero by using a small epsilon where naive_mae is 0
+ epsilon = 1e-10
+ mase_values = forecast_mae / np.maximum(naive_mae, epsilon)
+
+ # Apply reduction
+ if reduction == "mean":
+ return float(np.mean(mase_values))
+ elif reduction == "none":
+ # Average over horizon and features, keep batch dimension
+ return np.mean(mase_values, axis=(1, 2))
+ else:
+ raise ValueError(f"reduction must be 'mean' or 'none', got '{reduction}'")
+
+
+def crps_from_quantiles(
+ y_true: np.ndarray,
+ quantile_preds: np.ndarray,
+ quantile_levels: list[float],
+ reduction: ReductionType = "mean",
+) -> float | np.ndarray:
+ """Calculate Continuous Ranked Probability Score from quantile predictions.
+
+ CRPS is a proper scoring rule for evaluating probabilistic forecasts.
+ It generalizes the Mean Absolute Error (MAE) to probabilistic predictions
+ by measuring the integrated squared difference between the predicted and
+ true cumulative distribution functions.
+
+ This implementation uses the quantile approximation method, which estimates
+ CRPS from a discrete set of quantile predictions.
+
+ Mathematical Formula (Quantile Approximation):
+ CRPS โ ฮฃ_i w_i * |y_true - q_i| * (2 * I(y_true < q_i) - 1 - ฮฑ_i)
+
+ where:
+ - q_i is the i-th quantile prediction at level ฮฑ_i
+ - w_i is the weight for quantile i
+ - I(ยท) is the indicator function
+
+ Simplified approximation used here:
+ CRPS โ ฮฃ_i w_i * |y_true - q_i|
+
+ where w_i = ฮฑ_{i+1} - ฮฑ_{i-1} for interior quantiles.
+
+ Interpretation:
+ - CRPS = 0: Perfect probabilistic forecast
+ - Lower CRPS: Better forecast
+ - CRPS reduces to MAE when only median (0.5) quantile is provided
+ - CRPS penalizes both bias and miscalibration
+
+ Args:
+ y_true: Ground truth values. Shape: (batch_size, horizon, features).
+ quantile_preds: Quantile predictions. Shape: (batch_size, horizon, num_quantiles).
+ Quantiles should be ordered from lowest to highest level.
+ quantile_levels: List of quantile levels corresponding to quantile_preds.
+ Must have length equal to num_quantiles dimension.
+ Values should be in [0, 1] and sorted in ascending order.
+ Example: [0.1, 0.5, 0.9] for 10th, 50th, and 90th percentiles.
+ reduction: How to aggregate results across the batch dimension.
+ - 'mean': Return single scalar averaged across batch (default)
+ - 'none': Return per-sample metrics with shape (batch_size,)
+
+ Returns:
+ CRPS value(s). Returns float if reduction='mean', otherwise array of shape
+ (batch_size,) containing i for each sample in the batch.
+
+ Raises:
+ TypeError: If inputs are not numpy arrays or quantile_levels is not a list.
+ ValueError: If inputs have incompatible shapes, quantile_levels are invalid,
+ or arrays are empty.
+
+ Examples:
+ >>> import numpy as np
+ >>> from faim_sdk.eval import crps_from_quantiles
+ >>>
+ >>> # Example with known quantiles
+ >>> y_true = np.array([[[5.0], [6.0], [7.0]]]) # (1, 3, 1)
+ >>> # Quantile predictions: 10th, 50th, 90th percentiles
+ >>> quantile_preds = np.array([
+ ... [[4.5, 5.0, 5.5], # t=0
+ ... [5.5, 6.0, 6.5], # t=1
+ ... [6.5, 7.0, 7.5]] # t=2
+ ... ]) # (1, 3, 3)
+ >>> quantile_levels = [0.1, 0.5, 0.9]
+ >>>
+ >>> crps_score = crps_from_quantiles(
+ ... y_true,
+ ... quantile_preds,
+ ... quantile_levels,
+ ... reduction='mean'
+ ... )
+ >>> print(f"CRPS: {crps_score:.4f}")
+ >>>
+ >>> # Typical usage with FAIM SDK response
+ >>> from faim_sdk import ForecastClient, Chronos2ForecastRequest
+ >>> from faim_client.models import ModelName
+ >>>
+ >>> client = ForecastClient(base_url="https://api.example.com")
+ >>> request = Chronos2ForecastRequest(
+ ... x=train_data, # (32, 100, 1)
+ ... horizon=24,
+ ... output_type="quantiles",
+ ... quantiles=[0.1, 0.25, 0.5, 0.75, 0.9]
+ ... )
+ >>> response = client.forecast(ModelName.CHRONOS2, request)
+ >>>
+ >>> # Note: response.quantiles is (batch, horizon, num_quantiles)
+ >>> # but y_true is (batch, horizon, features)
+ >>> # For single-feature case, this works directly:
+ >>> crps = crps_from_quantiles(
+ ... test_data, # (32, 24, 1)
+ ... response.quantiles, # (32, 24, 5)
+ ... quantile_levels=[0.1, 0.25, 0.5, 0.75, 0.9]
+ ... )
+ >>>
+ >>> # Per-sample CRPS for analysis
+ >>> crps_per_sample = crps_from_quantiles(
+ ... test_data,
+ ... response.quantiles,
+ ... quantile_levels=[0.1, 0.25, 0.5, 0.75, 0.9],
+ ... reduction='none'
+ ... )
+ >>> print(f"Best sample CRPS: {crps_per_sample.min():.4f}")
+ >>> print(f"Worst sample CRPS: {crps_per_sample.max():.4f}")
+
+ Notes:
+ - CRPS is a proper scoring rule (incentivizes honest probabilistic forecasts)
+ - More quantiles generally provide better CRPS approximation
+ - CRPS is in the same units as the original data
+ - For purely point forecasts, use MSE or MASE instead
+ - This approximation becomes more accurate with more quantiles
+
+ References:
+ Gneiting, T., & Raftery, A. E. (2007). Strictly proper scoring rules,
+ prediction, and estimation. Journal of the American Statistical
+ Association, 102(477), 359-378.
+ """
+ # Type validation
+ if not isinstance(y_true, np.ndarray):
+ raise TypeError(f"y_true must be numpy.ndarray, got {type(y_true).__name__}")
+ if not isinstance(quantile_preds, np.ndarray):
+ raise TypeError(f"quantile_preds must be numpy.ndarray, got {type(quantile_preds).__name__}")
+ if not isinstance(quantile_levels, list):
+ raise TypeError(f"quantile_levels must be a list, got {type(quantile_levels).__name__}")
+
+ # Shape validation
+ if y_true.ndim != 3:
+ raise ValueError(f"y_true must be 3-dimensional (batch_size, horizon, features), got shape {y_true.shape}")
+ if quantile_preds.ndim != 3:
+ raise ValueError(
+ f"quantile_preds must be 3-dimensional (batch_size, horizon, num_quantiles), "
+ f"got shape {quantile_preds.shape}"
+ )
+
+ batch_size_true, horizon_true, features = y_true.shape
+ batch_size_pred, horizon_pred, num_quantiles = quantile_preds.shape
+
+ if batch_size_true != batch_size_pred:
+ raise ValueError(
+ f"Batch size mismatch: y_true has {batch_size_true} samples, quantile_preds has {batch_size_pred} samples"
+ )
+ if horizon_true != horizon_pred:
+ raise ValueError(f"Horizon mismatch: y_true has {horizon_true} steps, quantile_preds has {horizon_pred} steps")
+
+ # Quantile levels validation
+ if len(quantile_levels) != num_quantiles:
+ raise ValueError(
+ f"quantile_levels length ({len(quantile_levels)}) must match num_quantiles dimension ({num_quantiles})"
+ )
+
+ if not all(0.0 <= q <= 1.0 for q in quantile_levels):
+ raise ValueError(f"quantile_levels must be in [0.0, 1.0], got {quantile_levels}")
+
+ if quantile_levels != sorted(quantile_levels):
+ raise ValueError(f"quantile_levels must be sorted in ascending order, got {quantile_levels}")
+
+ # Empty validation
+ if y_true.size == 0:
+ raise ValueError("y_true cannot be empty")
+ if quantile_preds.size == 0:
+ raise ValueError("quantile_preds cannot be empty")
+
+ # For multi-feature forecasts, we need to ensure compatibility
+ # Typically, quantile forecasts are per-feature, so we expect:
+ # - If features == 1 and num_quantiles > 1: standard quantile forecast
+ # - If features == num_quantiles: each quantile corresponds to a feature (unusual)
+ # We'll handle the standard case where features == 1 or we average across features
+
+ if features != 1 and features != num_quantiles:
+ raise ValueError(
+ f"For CRPS calculation, y_true features ({features}) should be 1 "
+ f"or match num_quantiles ({num_quantiles}). "
+ f"For multi-feature forecasts with quantiles, compute CRPS per feature separately."
+ )
+
+ # Calculate weights for quantile approximation
+ # Weights are the differences between adjacent quantile levels
+ quantile_array = np.array(quantile_levels)
+ weights = np.zeros(num_quantiles)
+
+ # Interior quantiles get weight (ฮฑ_{i+1} - ฮฑ_{i-1}) / 2
+ if num_quantiles == 1:
+ weights[0] = 1.0
+ elif num_quantiles == 2:
+ weights[0] = quantile_array[1] - quantile_array[0]
+ weights[1] = quantile_array[1] - quantile_array[0]
+ else:
+ # First quantile
+ weights[0] = quantile_array[1] - quantile_array[0]
+ # Interior quantiles
+ for i in range(1, num_quantiles - 1):
+ weights[i] = (quantile_array[i + 1] - quantile_array[i - 1]) / 2.0
+ # Last quantile
+ weights[-1] = quantile_array[-1] - quantile_array[-2]
+
+ # Normalize weights to sum to 1
+ weights = weights / weights.sum()
+
+ # Reshape for broadcasting: (1, 1, num_quantiles)
+ weights_broadcast = weights.reshape(1, 1, -1)
+
+ # For standard case where features=1, expand y_true to match quantile dimension
+ if features == 1:
+ # Expand y_true from (batch, horizon, 1) to (batch, horizon, num_quantiles)
+ y_true_expanded = np.repeat(y_true, num_quantiles, axis=2)
+ else:
+ # features == num_quantiles case
+ y_true_expanded = y_true
+
+ # Calculate weighted absolute errors
+ absolute_errors = np.abs(y_true_expanded - quantile_preds)
+ weighted_errors = absolute_errors * weights_broadcast
+
+ # Sum over quantiles to get CRPS per (batch, horizon) point
+ crps_values = np.sum(weighted_errors, axis=2) # Shape: (batch_size, horizon)
+
+ # Apply reduction
+ if reduction == "mean":
+ return float(np.mean(crps_values))
+ elif reduction == "none":
+ # Average over horizon, keep batch dimension
+ return np.mean(crps_values, axis=1)
+ else:
+ raise ValueError(f"reduction must be 'mean' or 'none', got '{reduction}'")
diff --git a/faim_sdk/eval/py.typed b/faim_sdk/eval/py.typed
new file mode 100644
index 0000000..e69de29
diff --git a/faim_sdk/eval/visualization.py b/faim_sdk/eval/visualization.py
new file mode 100644
index 0000000..f5ac3f8
--- /dev/null
+++ b/faim_sdk/eval/visualization.py
@@ -0,0 +1,395 @@
+"""Visualization utilities for time-series forecasts.
+
+This module provides production-ready plotting functions for visualizing
+forecasting results. All functions use matplotlib for maximum compatibility.
+
+To use visualization features, install with the viz extra:
+ pip install faim-sdk[viz]
+
+Typical Usage:
+ >>> from faim_sdk import ForecastClient, Chronos2ForecastRequest
+ >>> from faim_sdk.eval import plot_forecast
+ >>> from faim_client.models import ModelName
+ >>> import numpy as np
+ >>>
+ >>> # Generate forecast
+ >>> client = ForecastClient()
+ >>> request = Chronos2ForecastRequest(
+ ... x=train_data, # shape: (32, 100, 1)
+ ... horizon=24,
+ ... output_type="point"
+ ... )
+ >>> response = client.forecast(ModelName.CHRONOS2, request)
+ >>>
+ >>> # Plot single sample from batch (omit batch dimension as documented)
+ >>> fig, ax = plot_forecast(
+ ... train_data=train_data[0], # (100, 1) - 2D array
+ ... forecast=response.point[0], # (24, 1) - 2D array
+ ... test_data=test_data[0], # (24, 1) - 2D array (optional)
+ ... title="Forecast for Sample 1"
+ ... )
+ >>> fig.savefig("forecast.png")
+
+Available Functions:
+ - plot_forecast: Plot training data, forecast, and optional test data
+"""
+
+from collections.abc import Sequence
+
+import numpy as np
+
+# matplotlib is an optional dependency
+try:
+ import matplotlib.pyplot as plt
+ from matplotlib.axes import Axes
+ from matplotlib.figure import Figure
+
+ MATPLOTLIB_AVAILABLE = True
+except ImportError:
+ MATPLOTLIB_AVAILABLE = False
+ Figure = None
+ Axes = None
+
+
+def plot_forecast(
+ train_data: np.ndarray,
+ forecast: np.ndarray,
+ test_data: np.ndarray | None = None,
+ features_on_same_plot: bool = True,
+ feature_names: Sequence[str] | None = None,
+ title: str | None = None,
+ figsize: tuple[float, float] | None = None,
+ save_path: str | None = None,
+) -> tuple[Figure, Axes | np.ndarray]:
+ """Plot time-series forecast with training data and optional test data.
+
+ Creates a visualization showing:
+ - Historical training data (solid line)
+ - Forecast predictions (dashed line)
+ - Optional test/ground truth data (solid line, different color)
+
+ **IMPORTANT**: All input arrays must be 2D (omit batch dimension).
+ Input shapes: (sequence_length, features)
+
+ Args:
+ train_data: Historical training data. Shape: (train_length, features).
+ Must be 2D array (batch dimension omitted).
+ forecast: Forecast predictions. Shape: (horizon, features).
+ Must be 2D array (batch dimension omitted).
+ test_data: Optional ground truth test data. Shape: (horizon, features).
+ If provided, must match forecast shape.
+ features_on_same_plot: How to handle multiple features.
+ - True: Plot all features on a single figure with unique colors (default)
+ - False: Create subplots, one per feature
+ feature_names: Optional list of feature names for legend.
+ Length must match number of features.
+ If None, features are named "Feature 1", "Feature 2", etc.
+ title: Optional plot title. If None, uses "Time Series Forecast".
+ figsize: Optional figure size (width, height) in inches.
+ If None, uses matplotlib defaults or (12, 6) for subplots.
+ save_path: Optional file path to save the figure.
+ Supports common formats: .png, .pdf, .svg, .jpg
+
+ Returns:
+ Tuple of (Figure, Axes). For features_on_same_plot=True, Axes is a single
+ Axes object. For features_on_same_plot=False, Axes is a numpy array of
+ Axes objects (one per subplot).
+
+ Raises:
+ ImportError: If matplotlib is not installed. Install with: pip install faim-sdk[viz]
+ TypeError: If inputs are not numpy arrays.
+ ValueError: If inputs have wrong dimensions, incompatible shapes,
+ are empty, or have too many features for single-plot visualization.
+
+ Examples:
+ >>> import numpy as np
+ >>> from faim_sdk.eval import plot_forecast
+ >>>
+ >>> # Single feature example
+ >>> train = np.random.randn(100, 1) # 100 time steps, 1 feature
+ >>> forecast = np.random.randn(24, 1) # 24 forecast steps
+ >>> test = np.random.randn(24, 1) # Ground truth
+ >>>
+ >>> fig, ax = plot_forecast(train, forecast, test, title="Single Feature")
+ >>> fig.savefig("single_feature.png")
+ >>>
+ >>> # Multi-feature on same plot
+ >>> train = np.random.randn(100, 3) # 3 features
+ >>> forecast = np.random.randn(24, 3)
+ >>> test = np.random.randn(24, 3)
+ >>>
+ >>> fig, ax = plot_forecast(
+ ... train,
+ ... forecast,
+ ... test,
+ ... features_on_same_plot=True,
+ ... feature_names=["Temperature", "Humidity", "Pressure"],
+ ... title="Weather Forecast"
+ ... )
+ >>>
+ >>> # Multi-feature with subplots
+ >>> fig, axes = plot_forecast(
+ ... train,
+ ... forecast,
+ ... test,
+ ... features_on_same_plot=False,
+ ... feature_names=["Temperature", "Humidity", "Pressure"],
+ ... figsize=(12, 8)
+ ... )
+ >>> # axes is array of 3 Axes objects
+ >>> fig.savefig("weather_subplots.png")
+ >>>
+ >>> # Usage with FAIM SDK (remember to index batch dimension)
+ >>> from faim_sdk import ForecastClient, Chronos2ForecastRequest
+ >>> from faim_client.models import ModelName
+ >>>
+ >>> client = ForecastClient()
+ >>> request = Chronos2ForecastRequest(
+ ... x=train_batch, # (32, 100, 1)
+ ... horizon=24,
+ ... output_type="point"
+ ... )
+ >>> response = client.forecast(ModelName.CHRONOS2, request)
+ >>>
+ >>> # Plot first sample from batch
+ >>> fig, ax = plot_forecast(
+ ... train_data=train_batch[0], # (100, 1) - Remove batch dim!
+ ... forecast=response.point[0], # (24, 1)
+ ... test_data=test_batch[0], # (24, 1)
+ ... )
+ >>>
+ >>> # Plot multiple samples in a loop
+ >>> for i in range(5):
+ ... fig, ax = plot_forecast(
+ ... train_batch[i],
+ ... response.point[i],
+ ... test_batch[i],
+ ... title=f"Sample {i+1}",
+ ... save_path=f"forecast_sample_{i+1}.png"
+ ... )
+
+ Notes:
+ - Always pass 2D arrays (omit batch dimension when indexing SDK responses)
+ - Maximum 10 features allowed when features_on_same_plot=True
+ - For more than 10 features, use features_on_same_plot=False
+ - Each feature gets a unique color automatically
+ - Training data shown in solid lines, forecasts in dashed lines
+ - Vertical line marks the train/forecast boundary
+ - Legend automatically includes all features and data types
+
+ Warnings:
+ If features_on_same_plot=True and number of features > 10, raises ValueError
+ to prevent cluttered visualizations.
+ """
+ # Check matplotlib availability
+ if not MATPLOTLIB_AVAILABLE:
+ raise ImportError(
+ "matplotlib is required for visualization. "
+ "Install with: pip install faim-sdk[viz] or pip install matplotlib"
+ )
+
+ # Type validation
+ if not isinstance(train_data, np.ndarray):
+ raise TypeError(f"train_data must be numpy.ndarray, got {type(train_data).__name__}")
+ if not isinstance(forecast, np.ndarray):
+ raise TypeError(f"forecast must be numpy.ndarray, got {type(forecast).__name__}")
+ if test_data is not None and not isinstance(test_data, np.ndarray):
+ raise TypeError(f"test_data must be numpy.ndarray or None, got {type(test_data).__name__}")
+
+ # Shape validation
+ if train_data.ndim != 2:
+ raise ValueError(
+ f"train_data must be 2-dimensional (sequence_length, features), got shape {train_data.shape}. "
+ f"Did you forget to index the batch dimension? Use train_data[i] to select a single sample."
+ )
+ if forecast.ndim != 2:
+ raise ValueError(
+ f"forecast must be 2-dimensional (horizon, features), got shape {forecast.shape}. "
+ f"Did you forget to index the batch dimension? Use forecast[i] to select a single sample."
+ )
+ if test_data is not None and test_data.ndim != 2:
+ raise ValueError(
+ f"test_data must be 2-dimensional (horizon, features), got shape {test_data.shape}. "
+ f"Did you forget to index the batch dimension? Use test_data[i] to select a single sample."
+ )
+
+ # Empty validation
+ if train_data.size == 0:
+ raise ValueError("train_data cannot be empty")
+ if forecast.size == 0:
+ raise ValueError("forecast cannot be empty")
+
+ # Extract dimensions
+ train_length, train_features = train_data.shape
+ horizon, forecast_features = forecast.shape
+
+ # Feature count validation
+ if train_features != forecast_features:
+ raise ValueError(
+ f"Feature count mismatch: train_data has {train_features} features, "
+ f"forecast has {forecast_features} features"
+ )
+
+ num_features = train_features
+
+ # Validate test_data if provided
+ if test_data is not None:
+ test_horizon, test_features = test_data.shape
+ if test_horizon != horizon:
+ raise ValueError(f"Horizon mismatch: forecast has {horizon} steps, test_data has {test_horizon} steps")
+ if test_features != num_features:
+ raise ValueError(
+ f"Feature count mismatch: forecast has {num_features} features, test_data has {test_features} features"
+ )
+
+ # Validate feature count for single plot
+ if features_on_same_plot and num_features > 10:
+ raise ValueError(
+ f"Cannot plot {num_features} features on same plot (maximum 10). "
+ f"Use features_on_same_plot=False to create subplots instead."
+ )
+
+ # Generate feature names if not provided
+ if feature_names is None:
+ if num_features == 1:
+ feature_names = ["Series"]
+ else:
+ feature_names = [f"Feature {i + 1}" for i in range(num_features)]
+ else:
+ if len(feature_names) != num_features:
+ raise ValueError(
+ f"feature_names length ({len(feature_names)}) must match number of features ({num_features})"
+ )
+
+ # Create time indices
+ train_indices = np.arange(train_length)
+ forecast_indices = np.arange(train_length, train_length + horizon)
+
+ # Set up figure
+ if features_on_same_plot:
+ # Single plot for all features
+ if figsize is None:
+ fig, ax = plt.subplots(figsize=(10, 6))
+ else:
+ fig, ax = plt.subplots(figsize=figsize)
+ axes = ax # Return single Axes object
+ else:
+ # Subplots for each feature
+ if figsize is None:
+ figsize = (12, 3 * num_features)
+ fig, axes_array = plt.subplots(num_features, 1, figsize=figsize, squeeze=False)
+ axes = axes_array.flatten() # Return array of Axes objects
+
+ # Define color palette (matplotlib default colors)
+ colors = plt.cm.tab10(np.linspace(0, 1, 10)) # 10 distinct colors
+
+ # Plot data
+ if features_on_same_plot:
+ # Plot all features on same axes
+ for i in range(num_features):
+ color = colors[i % 10]
+
+ # Training data
+ ax.plot(
+ train_indices,
+ train_data[:, i],
+ label=f"{feature_names[i]} (train)",
+ color=color,
+ linewidth=2,
+ alpha=0.8,
+ )
+
+ # Forecast
+ ax.plot(
+ forecast_indices,
+ forecast[:, i],
+ label=f"{feature_names[i]} (forecast)",
+ color=color,
+ linewidth=2,
+ linestyle="--",
+ alpha=0.8,
+ )
+
+ # Test data (if provided)
+ if test_data is not None:
+ ax.plot(
+ forecast_indices,
+ test_data[:, i],
+ label=f"{feature_names[i]} (actual)",
+ color=color,
+ linewidth=2,
+ linestyle=":",
+ alpha=0.6,
+ )
+
+ # Add vertical line at train/forecast boundary
+ ax.axvline(x=train_length, color="gray", linestyle="-", linewidth=1, alpha=0.5)
+
+ # Labels and legend
+ ax.set_xlabel("Time Step", fontsize=12)
+ ax.set_ylabel("Value", fontsize=12)
+ ax.set_title(title if title else "Time Series Forecast", fontsize=14, fontweight="bold")
+ ax.legend(loc="best", fontsize=10)
+ ax.grid(True, alpha=0.3)
+
+ else:
+ # Plot each feature on separate subplot
+ for i in range(num_features):
+ ax_i = axes[i]
+ color = colors[i % 10]
+
+ # Training data
+ ax_i.plot(
+ train_indices,
+ train_data[:, i],
+ label="Training",
+ color=color,
+ linewidth=2,
+ alpha=0.8,
+ )
+
+ # Forecast
+ ax_i.plot(
+ forecast_indices,
+ forecast[:, i],
+ label="Forecast",
+ color=color,
+ linewidth=2,
+ linestyle="--",
+ alpha=0.8,
+ )
+
+ # Test data (if provided)
+ if test_data is not None:
+ ax_i.plot(
+ forecast_indices,
+ test_data[:, i],
+ label="Actual",
+ color=color,
+ linewidth=2,
+ linestyle=":",
+ alpha=0.6,
+ )
+
+ # Add vertical line at train/forecast boundary
+ ax_i.axvline(x=train_length, color="gray", linestyle="-", linewidth=1, alpha=0.5)
+
+ # Labels and legend
+ ax_i.set_xlabel("Time Step", fontsize=11)
+ ax_i.set_ylabel("Value", fontsize=11)
+ ax_i.set_title(feature_names[i], fontsize=12, fontweight="bold")
+ ax_i.legend(loc="best", fontsize=9)
+ ax_i.grid(True, alpha=0.3)
+
+ # Overall title
+ if title:
+ fig.suptitle(title, fontsize=14, fontweight="bold", y=0.995)
+
+ # Tight layout
+ fig.tight_layout()
+
+ # Save if path provided
+ if save_path is not None:
+ fig.savefig(save_path, dpi=300, bbox_inches="tight")
+
+ return fig, axes
diff --git a/faim_sdk/exceptions.py b/faim_sdk/exceptions.py
index ed805b0..c4f9e83 100644
--- a/faim_sdk/exceptions.py
+++ b/faim_sdk/exceptions.py
@@ -1,10 +1,12 @@
"""Exceptions for FAIM SDK.
Provides a hierarchy of exceptions for precise error handling and debugging.
+Integrates with the unified error contract via generated ErrorResponse and ErrorCode.
"""
from typing import Any
+from faim_client.models.error_code import ErrorCode
from faim_client.models.error_response import ErrorResponse
@@ -27,6 +29,11 @@ def __init__(self, message: str, details: dict[str, Any] | None = None) -> None:
self.details = details or {}
def __str__(self) -> str:
+ """Return string representation of the error.
+
+ Returns:
+ Error message with details if available
+ """
if self.details:
return f"{self.message} (details: {self.details})"
return self.message
@@ -47,14 +54,15 @@ class SerializationError(FAIMError):
class APIError(FAIMError):
"""Base exception for API-related errors.
- Captures HTTP status codes and server error responses.
+ Captures HTTP status codes and server error responses with error contract integration.
+ All API errors include an optional ErrorResponse with machine-readable error codes.
"""
def __init__(
self,
message: str,
status_code: int | None = None,
- response: ErrorResponse | None = None,
+ error_response: ErrorResponse | None = None,
details: dict[str, Any] | None = None,
) -> None:
"""Initialize API error.
@@ -62,19 +70,43 @@ def __init__(
Args:
message: Human-readable error message
status_code: HTTP status code from response
- response: Parsed error response from backend
- details: Additional context
+ error_response: Parsed error response from backend (includes error_code, message, detail)
+ details: Additional context for debugging
"""
super().__init__(message, details)
self.status_code = status_code
- self.response = response
+ self.error_response = error_response
+
+ @property
+ def error_code(self) -> ErrorCode | None:
+ """Get machine-readable error code from error response.
+
+ Returns:
+ ErrorCode enum value if available, None otherwise
+
+ Example:
+ try:
+ client.forecast(...)
+ except ValidationError as e:
+ if e.error_code == ErrorCode.INVALID_SHAPE:
+ # Handle shape error specifically
+ pass
+ """
+ return self.error_response.error_code if self.error_response else None
def __str__(self) -> str:
+ """Return detailed string representation of the API error.
+
+ Returns:
+ Formatted string with message, status code, error code, request ID, and details
+ """
parts = [self.message]
if self.status_code:
parts.append(f"status={self.status_code}")
- if self.response:
- parts.append(f"response={self.response}")
+ if self.error_response:
+ parts.append(f"error_code={self.error_response.error_code}")
+ if self.error_response.request_id:
+ parts.append(f"request_id={self.error_response.request_id}")
if self.details:
parts.append(f"details={self.details}")
return " | ".join(parts)
@@ -84,8 +116,13 @@ class ModelNotFoundError(APIError):
"""Raised when specified model or version doesn't exist (404).
Check that:
- - Model name is valid (e.g., 'flowstate', 'toto')
+ - Model name is valid (e.g., 'flowstate', 'chronos2', 'tirex')
- Model version is deployed on backend
+
+ Error codes:
+ - MODEL_NOT_FOUND
+ - PRICING_NOT_FOUND
+ - RESOURCE_NOT_FOUND
"""
pass
@@ -107,10 +144,19 @@ class ValidationError(APIError):
"""Raised when backend rejects request as invalid (422).
Common causes:
- - Missing required parameters (e.g., horizon, x)
+ - Missing required parameters (e.g., horizon, x, output_type)
- Invalid parameter values
- Incompatible array shapes
- Model-specific parameter errors
+
+ Error codes:
+ - VALIDATION_ERROR
+ - INVALID_MODEL_INPUT
+ - INVALID_PARAMETER
+ - MISSING_REQUIRED_FIELD
+ - INVALID_SHAPE
+ - INVALID_DTYPE
+ - INVALID_VALUE_RANGE
"""
pass
@@ -152,11 +198,73 @@ class TimeoutError(FAIMError):
class AuthenticationError(APIError):
- """Raised when authentication fails (401).
+ """Raised when authentication fails (401, 403).
Check that:
- - Token is valid and not expired
- - Token has required permissions
+ - API key is valid and not expired
+ - API key has required permissions
+
+ Error codes:
+ - AUTHENTICATION_REQUIRED
+ - AUTHENTICATION_FAILED
+ - INVALID_API_KEY
+ - AUTHORIZATION_FAILED
+ """
+
+ pass
+
+
+class InsufficientFundsError(APIError):
+ """Raised when billing account balance is insufficient (402).
+
+ This indicates the user's account doesn't have enough credits
+ to perform the requested inference.
+
+ Actions:
+ - Add credits to billing account
+ - Check pricing for the model/version being used
+
+ Error codes:
+ - INSUFFICIENT_FUNDS
+ - BILLING_TRANSACTION_FAILED
+ """
+
+ pass
+
+
+class RateLimitError(APIError):
+ """Raised when rate limit is exceeded (429).
+
+ The user has made too many requests in a short period.
+
+ Actions:
+ - Implement exponential backoff retry logic
+ - Reduce request frequency
+ - Contact support for rate limit increases
+
+ Error codes:
+ - RATE_LIMIT_EXCEEDED
+ """
+
+ pass
+
+
+class ServiceUnavailableError(APIError):
+ """Raised when service is temporarily unavailable (503, 504).
+
+ This typically indicates transient infrastructure issues that
+ may be resolved by retrying with exponential backoff.
+
+ Common causes:
+ - Triton server connection failures
+ - GPU/CPU resources exhausted
+ - Out of memory conditions
+
+ Error codes:
+ - TRITON_CONNECTION_ERROR
+ - RESOURCE_EXHAUSTED
+ - OUT_OF_MEMORY
+ - TIMEOUT_ERROR (504)
"""
pass
diff --git a/faim_sdk/models.py b/faim_sdk/models.py
index 47736ad..8ef2961 100644
--- a/faim_sdk/models.py
+++ b/faim_sdk/models.py
@@ -5,10 +5,12 @@
"""
from dataclasses import dataclass, field
-from typing import Any, Literal
+from typing import Any, ClassVar, Literal
import numpy as np
+from faim_client.models import ModelName
+
# Type alias for output types
OutputType = Literal["point", "quantiles", "samples"]
@@ -18,12 +20,15 @@ class ForecastRequest:
"""Base forecast request with common parameters.
This is the base class for all model-specific forecast requests.
- Use model-specific subclasses (ToToForecastRequest, FlowStateForecastRequest)
+ Use model-specific subclasses (FlowStateForecastRequest, Chronos2ForecastRequest, TiRexForecastRequest)
for better type safety and IDE support.
"""
+ # Class variable to be overridden by subclasses
+ _model_name: ClassVar[ModelName]
+
x: np.ndarray
- """Time series data. Shape: (batch_size, sequence_length, features) or (sequence_length, features)"""
+ """Time series data. Shape: (batch_size, sequence_length, features)"""
horizon: int
"""Forecast horizon length (number of time steps to predict)"""
@@ -34,14 +39,42 @@ class ForecastRequest:
compression: str | None = "zstd"
"""Arrow compression algorithm. Options: 'zstd', 'lz4', None. Default: 'zstd'"""
+ @property
+ def model_name(self) -> ModelName:
+ """Get the model name for this request type.
+
+ Returns:
+ ModelName enum value indicating which model to use
+
+ Example:
+ >>> request = Chronos2ForecastRequest(x=data, horizon=10)
+ >>> print(request.model_name) # ModelName.CHRONOS2
+ """
+ return self._model_name
+
def __post_init__(self) -> None:
- """Validate common parameters."""
+ """Validate common parameters.
+
+ Automatically called after dataclass initialization to ensure
+ all parameters meet requirements.
+
+ Raises:
+ TypeError: If x is not a numpy ndarray
+ ValueError: If x is empty, not 3D, or horizon is non-positive
+ """
if not isinstance(self.x, np.ndarray):
raise TypeError(f"x must be numpy.ndarray, got {type(self.x).__name__}")
if self.x.size == 0:
raise ValueError("x cannot be empty")
+ # Ensure x is 3D: (batch_size, sequence_length, features)
+ if self.x.ndim != 3:
+ raise ValueError(
+ f"x must be a 3D array with shape (batch_size, sequence_length, features), "
+ f"got shape {self.x.shape} with {self.x.ndim} dimensions"
+ )
+
if self.horizon <= 0:
raise ValueError(f"horizon must be positive, got {self.horizon}")
@@ -64,73 +97,86 @@ def to_arrays_and_metadata(self) -> tuple[dict[str, np.ndarray], dict[str, Any]]
@dataclass
-class ToToForecastRequest(ForecastRequest):
- """Forecast request for ToTo model with probabilistic forecasting support.
+class Chronos2ForecastRequest(ForecastRequest):
+ """Forecast request for Chronos2 model.
- ToTo supports multi-series forecasting with padding masks and
- probabilistic predictions via sampling or quantiles.
+ Amazon Chronos 2.0 - Large language model for time series forecasting.
+ Supports point and quantile predictions.
"""
+ _model_name: ClassVar[ModelName] = ModelName.CHRONOS2
+
output_type: OutputType = "point"
"""Output type to return. Options: 'point', 'quantiles', 'samples'. Default: 'point'."""
- padding_mask: np.ndarray | None = None
- """Padding mask for variable-length sequences. Shape: same as x.
- 1 for valid timesteps, 0 for padding."""
-
- id_mask: np.ndarray | None = None
- """Identifier mask for multi-series forecasting. Shape: (batch_size,).
- Each unique ID represents a different time series."""
-
- num_samples: int | None = None
- """Number of forecast samples for probabilistic predictions.
- If set, returns sample-based distribution."""
-
quantiles: list[float] | None = None
"""Quantile levels for probabilistic forecasting.
- Example: [0.1, 0.5, 0.9] for 10th, 50th (median), 90th percentiles."""
+ Example: [0.1, 0.5, 0.9] for 10th, 50th (median), 90th percentiles.
+ Only used when output_type='quantiles'."""
def __post_init__(self) -> None:
- """Validate ToTo-specific parameters."""
- super().__post_init__()
-
- if self.padding_mask is not None:
- if not isinstance(self.padding_mask, np.ndarray):
- raise TypeError("padding_mask must be numpy.ndarray")
- if self.padding_mask.shape != self.x.shape:
- raise ValueError(f"padding_mask shape {self.padding_mask.shape} must match x shape {self.x.shape}")
+ """Validate Chronos2-specific parameters.
- if self.id_mask is not None:
- if not isinstance(self.id_mask, np.ndarray):
- raise TypeError("id_mask must be numpy.ndarray")
+ Automatically called after dataclass initialization to ensure
+ quantiles are valid probability values.
- if self.num_samples is not None and self.num_samples <= 0:
- raise ValueError(f"num_samples must be positive, got {self.num_samples}")
+ Raises:
+ ValueError: If quantiles are not in the range [0.0, 1.0]
+ """
+ super().__post_init__()
if self.quantiles is not None:
- if not all(0 <= q <= 1 for q in self.quantiles):
- raise ValueError(f"quantiles must be in [0, 1], got {self.quantiles}")
+ if not all(0.0 <= q <= 1.0 for q in self.quantiles):
+ raise ValueError(f"quantiles must be in [0.0, 1.0], got {self.quantiles}")
def to_arrays_and_metadata(self) -> tuple[dict[str, np.ndarray], dict[str, Any]]:
- """Convert ToTo request to Arrow format."""
- arrays, metadata = super().to_arrays_and_metadata()
+ """Convert Chronos2 request to Arrow format.
- # Add ToTo-specific arrays (large data)
- if self.padding_mask is not None:
- arrays["padding_mask"] = self.padding_mask
- if self.id_mask is not None:
- arrays["id_mask"] = self.id_mask
+ Separates request into large arrays (sent as Arrow columns) and
+ small metadata parameters (sent in Arrow schema metadata).
- # Add ToTo-specific metadata (small parameters)
+ Returns:
+ Tuple of (arrays dict, metadata dict) ready for Arrow serialization
+ """
+ arrays, metadata = super().to_arrays_and_metadata()
+
+ # Add Chronos2-specific metadata (small parameters)
metadata["output_type"] = self.output_type
- if self.num_samples is not None:
- metadata["num_samples"] = self.num_samples
if self.quantiles is not None:
metadata["quantiles"] = self.quantiles
return arrays, metadata
+@dataclass
+class TiRexForecastRequest(ForecastRequest):
+ """Forecast request for TiRex model.
+
+ TiRex - Transformer-based time series forecasting.
+ Supports point and quantile predictions.
+ """
+
+ _model_name: ClassVar[ModelName] = ModelName.TIREX
+
+ output_type: OutputType = "point"
+ """Output type to return. Options: 'point', 'quantiles', 'samples'. Default: 'point'."""
+
+ def to_arrays_and_metadata(self) -> tuple[dict[str, np.ndarray], dict[str, Any]]:
+ """Convert TiRex request to Arrow format.
+
+ Separates request into large arrays (sent as Arrow columns) and
+ small metadata parameters (sent in Arrow schema metadata).
+
+ Returns:
+ Tuple of (arrays dict, metadata dict) ready for Arrow serialization
+ """
+ arrays, metadata = super().to_arrays_and_metadata()
+
+ metadata["output_type"] = self.output_type
+
+ return arrays, metadata
+
+
@dataclass
class FlowStateForecastRequest(ForecastRequest):
"""Forecast request for FlowState model with scaling and prediction type control.
@@ -139,6 +185,8 @@ class FlowStateForecastRequest(ForecastRequest):
and different prediction modes.
"""
+ _model_name: ClassVar[ModelName] = ModelName.FLOWSTATE
+
output_type: OutputType = "point"
"""Output type to return. Options: 'point', 'quantiles', 'samples'. Default: 'point'."""
@@ -152,7 +200,15 @@ class FlowStateForecastRequest(ForecastRequest):
'quantile' (requires output_type='quantiles')."""
def __post_init__(self) -> None:
- """Validate FlowState-specific parameters."""
+ """Validate FlowState-specific parameters.
+
+ Automatically called after dataclass initialization to ensure
+ parameter validity and consistency between output_type and prediction_type.
+
+ Raises:
+ ValueError: If scale_factor is non-positive, or if output_type and
+ prediction_type are incompatible
+ """
super().__post_init__()
if self.scale_factor is not None and self.scale_factor <= 0:
@@ -170,6 +226,10 @@ def __post_init__(self) -> None:
raise ValueError(
f"prediction_type='{self.prediction_type}' requires output_type='point', got '{self.output_type}'"
)
+ elif self.output_type == "quantiles":
+ self.prediction_type = "quantile"
+ else:
+ self.prediction_type = "median"
# Validate output_type requires corresponding prediction_type
if self.output_type == "quantiles" and self.prediction_type != "quantile":
@@ -178,12 +238,17 @@ def __post_init__(self) -> None:
f"got prediction_type='{self.prediction_type}'"
)
if self.output_type == "point" and self.prediction_type == "quantile":
- raise ValueError(
- "output_type='point' conflicts with prediction_type='quantile'"
- )
+ raise ValueError("output_type='point' conflicts with prediction_type='quantile'")
def to_arrays_and_metadata(self) -> tuple[dict[str, np.ndarray], dict[str, Any]]:
- """Convert FlowState request to Arrow format."""
+ """Convert FlowState request to Arrow format.
+
+ Separates request into large arrays (sent as Arrow columns) and
+ small metadata parameters (sent in Arrow schema metadata).
+
+ Returns:
+ Tuple of (arrays dict, metadata dict) ready for Arrow serialization
+ """
arrays, metadata = super().to_arrays_and_metadata()
# Add FlowState-specific metadata
@@ -209,13 +274,13 @@ class ForecastResponse:
# Backend outputs
point: np.ndarray | None = None
- """Point predictions from FlowState. Shape: (batch_size, horizon, features)"""
+ """Point predictions. Shape: (batch_size, horizon, features)"""
quantiles: np.ndarray | None = None
- """Quantile predictions from ToTo. Shape: (batch_size, horizon, num_quantiles)"""
+ """Quantile predictions. Shape: (batch_size, horizon, num_quantiles, features)"""
samples: np.ndarray | None = None
- """Sample predictions from ToTo. Shape: (batch_size, horizon, num_samples)"""
+ """Sample predictions. Shape: (batch_size, horizon, num_samples)"""
@classmethod
def from_arrays_and_metadata(cls, arrays: dict[str, np.ndarray], metadata: dict[str, Any]) -> "ForecastResponse":
@@ -248,6 +313,11 @@ def from_arrays_and_metadata(cls, arrays: dict[str, np.ndarray], metadata: dict[
)
def __repr__(self) -> str:
+ """Return string representation of forecast response.
+
+ Returns:
+ Human-readable string showing available outputs and their shapes
+ """
outputs = []
if self.point is not None:
outputs.append(f"point.shape={self.point.shape}")
@@ -258,8 +328,4 @@ def __repr__(self) -> str:
outputs_str = ", ".join(outputs) if outputs else "None"
- return (
- f"ForecastResponse("
- f"outputs=[{outputs_str}], "
- f"metadata={self.metadata})"
- )
+ return f"ForecastResponse(outputs=[{outputs_str}], metadata={self.metadata})"
diff --git a/pyproject.toml b/pyproject.toml
new file mode 100644
index 0000000..b6bb9f7
--- /dev/null
+++ b/pyproject.toml
@@ -0,0 +1,40 @@
+[tool.poetry]
+name = "faim-sdk"
+version = "0.4.0"
+description = "Python SDK for FAIM time-series forecasting with foundation AI models"
+authors = ["FAIM "]
+license = "Apache-2.0"
+readme = "README.md"
+packages = [
+ { include = "faim_sdk" },
+ { include = "faim_client" },
+]
+include = ["CHANGELOG.md", "faim_client/py.typed", "faim_sdk/py.typed", "faim_sdk/eval/py.typed"]
+
+[tool.poetry.dependencies]
+python = "^3.11"
+numpy = ">=1.26.0,<2.0.0"
+pyarrow = ">=18.1.0"
+httpx = ">=0.23.0,<0.29.0"
+attrs = ">=22.2.0"
+matplotlib = ">=3.5.0"
+
+
+[build-system]
+requires = ["poetry-core>=2.0.0,<3.0.0"]
+build-backend = "poetry.core.masonry.api"
+
+[tool.poetry.group.dev.dependencies]
+jupyterlab = ">=4.4.10"
+pandas = ">=2.3.3"
+pytest = ">=8.0.0"
+pytest-asyncio = ">=0.23.0"
+pytest-cov = ">=4.1.0"
+ruff = ">=0.8.0"
+
+[tool.ruff]
+line-length = 120
+extend-exclude = ["faim_client/"]
+
+[tool.ruff.lint]
+select = ["F", "I", "UP"]
diff --git a/scripts/regenerate_client.sh b/scripts/regenerate_client.sh
new file mode 100755
index 0000000..53cb33b
--- /dev/null
+++ b/scripts/regenerate_client.sh
@@ -0,0 +1,162 @@
+#!/bin/bash
+set -e # Exit on error
+
+# FAIM Client Regeneration Script
+# This script regenerates the faim_client from openapi.json and cleans up unused API endpoints
+# Usage: ./scripts/regenerate_client.sh
+
+echo "========================================="
+echo "FAIM Client Regeneration Script"
+echo "========================================="
+echo ""
+
+# Check if openapi.json exists
+if [ ! -f "openapi.json" ]; then
+ echo "โ Error: openapi.json not found in current directory"
+ exit 1
+fi
+
+# Check if client.config.yaml exists
+if [ ! -f "client.config.yaml" ]; then
+ echo "โ Error: client.config.yaml not found in current directory"
+ exit 1
+fi
+
+echo "๐ Step 1: Generating client from OpenAPI spec..."
+echo " Using: openapi-python-client"
+echo ""
+
+# Run openapi-python-client
+openapi-python-client generate \
+ --path openapi.json \
+ --config client.config.yaml \
+ --overwrite \
+ --meta none
+
+if [ $? -ne 0 ]; then
+ echo "โ Client generation failed"
+ exit 1
+fi
+
+echo "โ
Client generated successfully"
+echo ""
+
+echo "๐งน Step 2: Cleaning up unused API endpoints..."
+echo ""
+
+# Track what we're removing
+removed_count=0
+
+# Remove user management API endpoints
+if [ -d "faim_client/api/user" ]; then
+ echo " Removing: faim_client/api/user/ (session management - unused)"
+ rm -rf faim_client/api/user
+ removed_count=$((removed_count + 1))
+fi
+
+# Remove API keys management endpoints
+if [ -d "faim_client/api/api_keys" ]; then
+ echo " Removing: faim_client/api/api_keys/ (API key management - unused)"
+ rm -rf faim_client/api/api_keys
+ removed_count=$((removed_count + 1))
+fi
+
+echo ""
+echo "๐งน Step 3: Cleaning up unused model files..."
+echo ""
+
+# Remove API key related models
+# Note: http_validation_error.py and validation_error.py are kept as they may be used by FastAPI validation
+models_to_remove=(
+ "api_key_info.py"
+ "create_api_key_request.py"
+ "create_api_key_response.py"
+ "get_all_api_keys_response.py"
+ "revoke_api_key_request.py"
+)
+
+for model_file in "${models_to_remove[@]}"; do
+ model_path="faim_client/models/$model_file"
+ if [ -f "$model_path" ]; then
+ echo " Removing: $model_path"
+ rm "$model_path"
+ removed_count=$((removed_count + 1))
+ fi
+done
+
+echo ""
+echo "โ
Cleanup complete ($removed_count items removed)"
+echo ""
+
+echo "๐ง Step 4: Fixing faim_client/models/__init__.py..."
+echo ""
+
+# Fix the __init__.py to remove imports of deleted models
+cat > faim_client/models/__init__.py << 'EOF'
+"""Contains all the data models used in inputs/outputs"""
+
+from .error_code import ErrorCode
+from .error_response import ErrorResponse
+from .http_validation_error import HTTPValidationError
+from .model_name import ModelName
+from .validation_error import ValidationError
+
+__all__ = (
+ "ErrorCode",
+ "ErrorResponse",
+ "HTTPValidationError",
+ "ModelName",
+ "ValidationError",
+)
+EOF
+
+echo " โ
Updated faim_client/models/__init__.py"
+echo ""
+
+echo "๐ Step 5: Verification..."
+echo ""
+
+# Verify key files exist
+echo " Checking generated files..."
+if [ -f "faim_client/models/model_name.py" ]; then
+ echo " โ
ModelName enum generated"
+else
+ echo " โ ModelName enum missing"
+fi
+
+if [ -f "faim_client/models/error_code.py" ]; then
+ echo " โ
ErrorCode enum generated"
+else
+ echo " โ ErrorCode enum missing"
+fi
+
+if [ -f "faim_client/models/error_response.py" ]; then
+ echo " โ
ErrorResponse model generated"
+else
+ echo " โ ErrorResponse model missing"
+fi
+
+if [ -d "faim_client/api/forecast" ]; then
+ echo " โ
Forecast API endpoint retained"
+else
+ echo " โ Forecast API endpoint missing"
+fi
+
+if [ -d "faim_client/api/health" ]; then
+ echo " โ
Health API endpoint retained"
+else
+ echo " โ ๏ธ Health API endpoint missing (optional)"
+fi
+
+echo ""
+echo "========================================="
+echo "โ
Regeneration Complete!"
+echo "========================================="
+echo ""
+echo "Generated client is ready at: faim_client/"
+echo ""
+echo "Next steps:"
+echo " 1. Review changes in faim_client/"
+echo " 2. Update faim_sdk/ if needed"
+echo " 3. Run tests to verify compatibility"
+echo ""
\ No newline at end of file
diff --git a/tests/unit/test_client.py b/tests/unit/test_client.py
new file mode 100644
index 0000000..58580a0
--- /dev/null
+++ b/tests/unit/test_client.py
@@ -0,0 +1,843 @@
+"""Unit tests for faim_sdk.client module.
+
+Tests ForecastClient initialization, sync/async methods, and error handling.
+"""
+
+from unittest.mock import Mock, patch
+
+import httpx
+import numpy as np
+import pytest
+
+from faim_client.models.error_code import ErrorCode
+from faim_client.models.error_response import ErrorResponse
+from faim_sdk.client import ForecastClient
+from faim_sdk.exceptions import (
+ AuthenticationError,
+ ConfigurationError,
+ InsufficientFundsError,
+ InternalServerError,
+ ModelNotFoundError,
+ NetworkError,
+ PayloadTooLargeError,
+ RateLimitError,
+ SerializationError,
+ ServiceUnavailableError,
+ TimeoutError,
+ ValidationError,
+)
+from faim_sdk.models import Chronos2ForecastRequest, FlowStateForecastRequest, ForecastResponse, TiRexForecastRequest
+from faim_sdk.utils import serialize_to_arrow
+
+
+class TestForecastClientInitialization:
+ """Tests for ForecastClient initialization."""
+
+ def test_initialization_minimal(self):
+ """Test initialization with minimal parameters."""
+ client = ForecastClient()
+
+ assert client.base_url == "https://api.faim.it.com"
+ assert client.timeout == 120.0
+ assert client._client is not None
+
+ def test_initialization_with_api_key(self):
+ """Test initialization with API key."""
+ client = ForecastClient(
+ api_key="test-key-123",
+ )
+
+ assert client.base_url == "https://api.faim.it.com"
+
+ def test_initialization_with_timeout(self):
+ """Test initialization with custom timeout."""
+ client = ForecastClient(
+ base_url="https://api.example.com",
+ timeout=60.0,
+ )
+
+ assert client.timeout == 60.0
+
+ def test_initialization_validation_requires_base_url(self):
+ """Test that base_url is required."""
+ with pytest.raises(ConfigurationError, match="base_url is required"):
+ ForecastClient(base_url="")
+
+ def test_initialization_validation_requires_valid_url(self):
+ """Test that base_url must be a valid URL."""
+ with pytest.raises(ConfigurationError, match="base_url must be a valid URL"):
+ ForecastClient(base_url="not-a-url")
+
+ def test_initialization_validation_positive_timeout(self):
+ """Test that timeout must be positive."""
+ with pytest.raises(ConfigurationError, match="timeout must be positive"):
+ ForecastClient(base_url="https://api.example.com", timeout=0)
+
+ def test_initialization_validation_negative_timeout(self):
+ """Test that timeout cannot be negative."""
+ with pytest.raises(ConfigurationError, match="timeout must be positive"):
+ ForecastClient(base_url="https://api.example.com", timeout=-5)
+
+ def test_context_manager_sync(self):
+ """Test synchronous context manager."""
+ with ForecastClient(base_url="https://api.example.com") as client:
+ assert client._client is not None
+
+ # Client should be closed after context
+ # (We can't easily test this without making actual requests)
+
+ @pytest.mark.asyncio
+ async def test_context_manager_async(self):
+ """Test asynchronous context manager."""
+ async with ForecastClient(base_url="https://api.example.com") as client:
+ assert client._client is not None
+
+
+class TestForecastClientForecast:
+ """Tests for ForecastClient.forecast() method."""
+
+ def setup_method(self):
+ """Set up test fixtures."""
+ self.client = ForecastClient(base_url="https://api.example.com")
+ self.test_data = np.random.rand(2, 10, 1).astype(np.float32)
+
+ @patch("httpx.Client.post")
+ def test_forecast_chronos2_point(self, mock_post):
+ """Test forecast with Chronos2 model for point predictions."""
+ # Setup mock response
+ response_arrays = {"point": np.random.rand(2, 5, 1).astype(np.float32)}
+ response_metadata = {"model_name": "chronos2", "model_version": "1"}
+ mock_response = Mock()
+ mock_response.status_code = 200
+ mock_response.content = serialize_to_arrow(response_arrays, response_metadata)
+ mock_post.return_value = mock_response
+
+ # Create request and call forecast
+ request = Chronos2ForecastRequest(
+ x=self.test_data,
+ horizon=5,
+ output_type="point",
+ )
+ response = self.client.forecast(request)
+
+ # Verify response
+ assert isinstance(response, ForecastResponse)
+ assert response.point is not None
+ assert response.point.shape == (2, 5, 1)
+ assert response.metadata["model_name"] == "chronos2"
+
+ # Verify request was made correctly
+ mock_post.assert_called_once()
+ call_args = mock_post.call_args
+ assert "chronos2" in call_args[1]["url"]
+
+ @patch("httpx.Client.post")
+ def test_forecast_chronos2_quantiles(self, mock_post):
+ """Test forecast with Chronos2 model for quantile predictions."""
+ # Setup mock response
+ response_arrays = {"quantiles": np.random.rand(2, 5, 3).astype(np.float32)}
+ response_metadata = {"model_name": "chronos2", "quantiles": [0.1, 0.5, 0.9]}
+ mock_response = Mock()
+ mock_response.status_code = 200
+ mock_response.content = serialize_to_arrow(response_arrays, response_metadata)
+ mock_post.return_value = mock_response
+
+ # Create request and call forecast
+ request = Chronos2ForecastRequest(
+ x=self.test_data,
+ horizon=5,
+ output_type="quantiles",
+ quantiles=[0.1, 0.5, 0.9],
+ )
+ response = self.client.forecast(request)
+
+ # Verify response
+ assert response.quantiles is not None
+ assert response.quantiles.shape == (2, 5, 3)
+ assert response.point is None
+
+ @patch("httpx.Client.post")
+ def test_forecast_flowstate(self, mock_post):
+ """Test forecast with FlowState model."""
+ # Setup mock response
+ response_arrays = {"point": np.random.rand(2, 5, 1).astype(np.float32)}
+ response_metadata = {"model_name": "flowstate"}
+ mock_response = Mock()
+ mock_response.status_code = 200
+ mock_response.content = serialize_to_arrow(response_arrays, response_metadata)
+ mock_post.return_value = mock_response
+
+ # Create request and call forecast
+ request = FlowStateForecastRequest(
+ x=self.test_data,
+ horizon=5,
+ output_type="point",
+ prediction_type="mean",
+ )
+ response = self.client.forecast(request)
+
+ # Verify response
+ assert response.point is not None
+ assert response.metadata["model_name"] == "flowstate"
+
+ # Verify URL contains flowstate
+ call_args = mock_post.call_args
+ assert "flowstate" in call_args[1]["url"]
+
+ @patch("httpx.Client.post")
+ def test_forecast_tirex(self, mock_post):
+ """Test forecast with TiRex model."""
+ # Setup mock response
+ response_arrays = {"point": np.random.rand(2, 5, 1).astype(np.float32)}
+ response_metadata = {"model_name": "tirex"}
+ mock_response = Mock()
+ mock_response.status_code = 200
+ mock_response.content = serialize_to_arrow(response_arrays, response_metadata)
+ mock_post.return_value = mock_response
+
+ # Create request and call forecast
+ request = TiRexForecastRequest(
+ x=self.test_data,
+ horizon=5,
+ output_type="point",
+ )
+ response = self.client.forecast(request)
+
+ # Verify response
+ assert response.point is not None
+
+ # Verify URL contains tirex
+ call_args = mock_post.call_args
+ assert "tirex" in call_args[1]["url"]
+
+ @patch("httpx.Client.post")
+ def test_forecast_with_custom_model_version(self, mock_post):
+ """Test forecast with custom model version."""
+ # Setup mock response
+ response_arrays = {"point": np.random.rand(2, 5, 1).astype(np.float32)}
+ mock_response = Mock()
+ mock_response.status_code = 200
+ mock_response.content = serialize_to_arrow(response_arrays, {})
+ mock_post.return_value = mock_response
+
+ # Create request with custom version
+ request = Chronos2ForecastRequest(
+ x=self.test_data,
+ horizon=5,
+ model_version="2.0",
+ )
+ self.client.forecast(request)
+
+ # Verify URL contains version
+ call_args = mock_post.call_args
+ assert "/2.0" in call_args[1]["url"] or "2.0" in call_args[1]["url"]
+
+
+class TestForecastClientErrorHandling:
+ """Tests for ForecastClient error handling."""
+
+ def setup_method(self):
+ """Set up test fixtures."""
+ self.client = ForecastClient(base_url="https://api.example.com")
+ self.test_data = np.random.rand(2, 10, 1).astype(np.float32)
+ self.request = Chronos2ForecastRequest(x=self.test_data, horizon=5)
+
+ @patch("httpx.Client.post")
+ def test_validation_error_422(self, mock_post):
+ """Test handling of 422 validation errors."""
+ # Setup mock error response
+ error_response = ErrorResponse(
+ error_code=ErrorCode.INVALID_SHAPE,
+ message="Shape validation failed",
+ detail="Expected (batch, seq, feat)",
+ request_id="req_123",
+ )
+ mock_response = Mock()
+ mock_response.status_code = 422
+ mock_response.json.return_value = error_response.to_dict()
+ mock_post.return_value = mock_response
+
+ with pytest.raises(ValidationError) as exc_info:
+ self.client.forecast(self.request)
+
+ assert exc_info.value.status_code == 422
+ assert exc_info.value.error_code == ErrorCode.INVALID_SHAPE
+
+ @patch("httpx.Client.post")
+ def test_authentication_error_401(self, mock_post):
+ """Test handling of 401 authentication errors."""
+ error_response = ErrorResponse(
+ error_code=ErrorCode.INVALID_API_KEY,
+ message="API key is invalid",
+ )
+ mock_response = Mock()
+ mock_response.status_code = 401
+ mock_response.json.return_value = error_response.to_dict()
+ mock_post.return_value = mock_response
+
+ with pytest.raises(AuthenticationError) as exc_info:
+ self.client.forecast(self.request)
+
+ assert exc_info.value.status_code == 401
+
+ @patch("httpx.Client.post")
+ def test_authentication_error_403(self, mock_post):
+ """Test handling of 403 forbidden errors."""
+ mock_response = Mock()
+ mock_response.status_code = 403
+ mock_response.json.return_value = {}
+ mock_post.return_value = mock_response
+
+ with pytest.raises(AuthenticationError) as exc_info:
+ self.client.forecast(self.request)
+
+ assert exc_info.value.status_code == 403
+
+ @patch("httpx.Client.post")
+ def test_insufficient_funds_error_402(self, mock_post):
+ """Test handling of 402 payment required errors."""
+ error_response = ErrorResponse(
+ error_code=ErrorCode.INSUFFICIENT_FUNDS,
+ message="Insufficient balance",
+ )
+ mock_response = Mock()
+ mock_response.status_code = 402
+ mock_response.json.return_value = error_response.to_dict()
+ mock_post.return_value = mock_response
+
+ with pytest.raises(InsufficientFundsError) as exc_info:
+ self.client.forecast(self.request)
+
+ assert exc_info.value.status_code == 402
+
+ @patch("httpx.Client.post")
+ def test_model_not_found_error_404(self, mock_post):
+ """Test handling of 404 not found errors."""
+ error_response = ErrorResponse(
+ error_code=ErrorCode.MODEL_NOT_FOUND,
+ message="Model not found",
+ )
+ mock_response = Mock()
+ mock_response.status_code = 404
+ mock_response.json.return_value = error_response.to_dict()
+ mock_post.return_value = mock_response
+
+ with pytest.raises(ModelNotFoundError) as exc_info:
+ self.client.forecast(self.request)
+
+ assert exc_info.value.status_code == 404
+
+ @patch("httpx.Client.post")
+ def test_payload_too_large_error_413(self, mock_post):
+ """Test handling of 413 payload too large errors."""
+ error_response = ErrorResponse(
+ error_code=ErrorCode.PAYLOAD_TOO_LARGE,
+ message="Payload exceeds limit",
+ )
+ mock_response = Mock()
+ mock_response.status_code = 413
+ mock_response.json.return_value = error_response.to_dict()
+ mock_post.return_value = mock_response
+
+ with pytest.raises(PayloadTooLargeError) as exc_info:
+ self.client.forecast(self.request)
+
+ assert exc_info.value.status_code == 413
+
+ @patch("httpx.Client.post")
+ def test_rate_limit_error_429(self, mock_post):
+ """Test handling of 429 rate limit errors."""
+ error_response = ErrorResponse(
+ error_code=ErrorCode.RATE_LIMIT_EXCEEDED,
+ message="Rate limit exceeded",
+ )
+ mock_response = Mock()
+ mock_response.status_code = 429
+ mock_response.json.return_value = error_response.to_dict()
+ mock_post.return_value = mock_response
+
+ with pytest.raises(RateLimitError) as exc_info:
+ self.client.forecast(self.request)
+
+ assert exc_info.value.status_code == 429
+
+ @patch("httpx.Client.post")
+ def test_internal_server_error_500(self, mock_post):
+ """Test handling of 500 internal server errors."""
+ mock_response = Mock()
+ mock_response.status_code = 500
+ mock_response.json.return_value = {}
+ mock_post.return_value = mock_response
+
+ with pytest.raises(InternalServerError) as exc_info:
+ self.client.forecast(self.request)
+
+ assert exc_info.value.status_code == 500
+
+ @patch("httpx.Client.post")
+ def test_service_unavailable_error_503(self, mock_post):
+ """Test handling of 503 service unavailable errors."""
+ error_response = ErrorResponse(
+ error_code=ErrorCode.TRITON_CONNECTION_ERROR,
+ message="Cannot connect to backend",
+ )
+ mock_response = Mock()
+ mock_response.status_code = 503
+ mock_response.json.return_value = error_response.to_dict()
+ mock_post.return_value = mock_response
+
+ with pytest.raises(ServiceUnavailableError) as exc_info:
+ self.client.forecast(self.request)
+
+ assert exc_info.value.status_code == 503
+
+ @patch("httpx.Client.post")
+ def test_service_unavailable_error_504(self, mock_post):
+ """Test handling of 504 gateway timeout errors."""
+ mock_response = Mock()
+ mock_response.status_code = 504
+ mock_response.json.return_value = {}
+ mock_post.return_value = mock_response
+
+ with pytest.raises(ServiceUnavailableError) as exc_info:
+ self.client.forecast(self.request)
+
+ assert exc_info.value.status_code == 504
+
+ @patch("httpx.Client.post")
+ def test_network_error_connection_failed(self, mock_post):
+ """Test handling of network connection errors."""
+ mock_post.side_effect = httpx.ConnectError("Connection refused")
+
+ with pytest.raises(NetworkError) as exc_info:
+ self.client.forecast(self.request)
+
+ assert "Connection refused" in str(exc_info.value)
+
+ @patch("httpx.Client.post")
+ def test_timeout_error(self, mock_post):
+ """Test handling of timeout errors."""
+ mock_post.side_effect = httpx.TimeoutException("Request timeout")
+
+ with pytest.raises(TimeoutError) as exc_info:
+ self.client.forecast(self.request)
+
+ assert "timeout" in str(exc_info.value).lower()
+
+ @patch("faim_sdk.utils.serialize_to_arrow")
+ def test_serialization_error(self, mock_serialize):
+ """Test handling of serialization errors."""
+ mock_serialize.side_effect = TypeError("Invalid array type")
+
+ with pytest.raises(SerializationError) as exc_info:
+ self.client.forecast(self.request)
+
+ assert "serialization" in str(exc_info.value).lower()
+
+
+class TestForecastClientAsync:
+ """Tests for ForecastClient.forecast_async() method."""
+
+ def setup_method(self):
+ """Set up test fixtures."""
+ self.client = ForecastClient(base_url="https://api.example.com")
+ self.test_data = np.random.rand(2, 10, 1).astype(np.float32)
+
+ @pytest.mark.asyncio
+ @patch("httpx.AsyncClient.post")
+ async def test_forecast_async_success(self, mock_post):
+ """Test async forecast with successful response."""
+ # Setup mock response
+ response_arrays = {"point": np.random.rand(2, 5, 1).astype(np.float32)}
+ response_metadata = {"model_name": "chronos2"}
+ mock_response = Mock()
+ mock_response.status_code = 200
+ mock_response.content = serialize_to_arrow(response_arrays, response_metadata)
+ mock_post.return_value = mock_response
+
+ # Create request and call async forecast
+ request = Chronos2ForecastRequest(
+ x=self.test_data,
+ horizon=5,
+ output_type="point",
+ )
+ response = await self.client.forecast_async(request)
+
+ # Verify response
+ assert isinstance(response, ForecastResponse)
+ assert response.point is not None
+ assert response.point.shape == (2, 5, 1)
+
+ @pytest.mark.asyncio
+ @patch("httpx.AsyncClient.post")
+ async def test_forecast_async_validation_error(self, mock_post):
+ """Test async forecast with validation error."""
+ # Setup mock error response
+ error_response = ErrorResponse(
+ error_code=ErrorCode.INVALID_SHAPE,
+ message="Shape validation failed",
+ )
+ mock_response = Mock()
+ mock_response.status_code = 422
+ mock_response.json.return_value = error_response.to_dict()
+ mock_post.return_value = mock_response
+
+ request = Chronos2ForecastRequest(x=self.test_data, horizon=5)
+
+ with pytest.raises(ValidationError):
+ await self.client.forecast_async(request)
+
+
+class TestForecastClientClose:
+ """Tests for ForecastClient resource cleanup."""
+
+ def test_close_method(self):
+ """Test close method closes underlying client."""
+ client = ForecastClient(base_url="https://api.example.com")
+
+ # Should not raise any exceptions
+ client.close()
+
+ @pytest.mark.asyncio
+ async def test_aclose_method(self):
+ """Test async close method."""
+ client = ForecastClient(base_url="https://api.example.com")
+
+ # Should not raise any exceptions
+ await client.aclose()
+
+ def test_context_manager_closes_client(self):
+ """Test that context manager closes client on exit."""
+ with ForecastClient(base_url="https://api.example.com"):
+ pass
+
+ # Client should be closed after context exits
+ # (We can't easily verify this without implementation details)
+
+ @pytest.mark.asyncio
+ async def test_async_context_manager_closes_client(self):
+ """Test that async context manager closes client on exit."""
+ async with ForecastClient(base_url="https://api.example.com"):
+ pass
+
+ # Client should be closed after context exits
+
+
+class TestForecastClientLogging:
+ """Tests for ForecastClient logging behavior."""
+
+ @patch("httpx.Client.post")
+ def test_logs_request_info(self, mock_post):
+ """Test that client logs request information."""
+ # Setup mock response
+ response_arrays = {"point": np.random.rand(2, 5, 1).astype(np.float32)}
+ mock_response = Mock()
+ mock_response.status_code = 200
+ mock_response.content = serialize_to_arrow(response_arrays, {})
+ mock_post.return_value = mock_response
+
+ client = ForecastClient(base_url="https://api.example.com")
+ request = Chronos2ForecastRequest(
+ x=np.random.rand(2, 10, 1).astype(np.float32),
+ horizon=5,
+ )
+
+ # Should not raise exceptions
+ with patch("faim_sdk.client.logger") as mock_logger:
+ client.forecast(request)
+ # Verify some logging occurred
+ assert mock_logger.debug.called or mock_logger.info.called
+
+ @patch("httpx.Client.post")
+ def test_logs_error_info(self, mock_post):
+ """Test that client logs error information."""
+ # Setup mock error response
+ mock_response = Mock()
+ mock_response.status_code = 422
+ mock_response.json.return_value = {}
+ mock_post.return_value = mock_response
+
+ client = ForecastClient(base_url="https://api.example.com")
+ request = Chronos2ForecastRequest(
+ x=np.random.rand(2, 10, 1).astype(np.float32),
+ horizon=5,
+ )
+
+ with patch("faim_sdk.client.logger") as mock_logger:
+ try:
+ client.forecast(request)
+ except ValidationError:
+ pass
+
+ # Verify error logging occurred
+ assert mock_logger.error.called or mock_logger.warning.called
+
+
+class TestForecastClientIntegration:
+ """Integration-style tests for realistic usage patterns."""
+
+ @patch("httpx.Client.post")
+ def test_multiple_requests_same_client(self, mock_post):
+ """Test making multiple requests with the same client instance."""
+ # Setup mock response
+ response_arrays = {"point": np.random.rand(2, 5, 1).astype(np.float32)}
+ mock_response = Mock()
+ mock_response.status_code = 200
+ mock_response.content = serialize_to_arrow(response_arrays, {})
+ mock_post.return_value = mock_response
+
+ client = ForecastClient(base_url="https://api.example.com")
+ data = np.random.rand(2, 10, 1).astype(np.float32)
+
+ # Make multiple requests
+ for _ in range(3):
+ request = Chronos2ForecastRequest(x=data, horizon=5)
+ response = client.forecast(request)
+ assert response.point is not None
+
+ # Verify all requests were made
+ assert mock_post.call_count == 3
+
+ @patch("httpx.Client.post")
+ def test_different_models_same_client(self, mock_post):
+ """Test using different models with the same client."""
+ # Setup mock response
+ response_arrays = {"point": np.random.rand(2, 5, 1).astype(np.float32)}
+ mock_response = Mock()
+ mock_response.status_code = 200
+ mock_response.content = serialize_to_arrow(response_arrays, {})
+ mock_post.return_value = mock_response
+
+ client = ForecastClient(base_url="https://api.example.com")
+ data = np.random.rand(2, 10, 1).astype(np.float32)
+
+ # Test different models
+ requests = [
+ Chronos2ForecastRequest(x=data, horizon=5),
+ FlowStateForecastRequest(x=data, horizon=5, prediction_type="mean"),
+ TiRexForecastRequest(x=data, horizon=5),
+ ]
+
+ for req in requests:
+ response = client.forecast(req)
+ assert response.point is not None
+
+ assert mock_post.call_count == 3
+
+
+class TestUnivariateTransformation:
+ """Tests for univariate transformation of FlowState and TiRex models."""
+
+ @patch("httpx.Client.post")
+ def test_flowstate_multivariate_point_forecast(self, mock_post):
+ """Test FlowState with multivariate input transforms and reshapes correctly for point forecast."""
+ # Setup: multivariate input (2, 10, 3) - 2 series, 10 timesteps, 3 features
+ data = np.random.rand(2, 10, 3).astype(np.float32)
+
+ # Mock server response: (batch*features=6, horizon=5, features=1)
+ response_arrays = {"point": np.random.rand(6, 5, 1).astype(np.float32)}
+ mock_response = Mock()
+ mock_response.status_code = 200
+ mock_response.content = serialize_to_arrow(response_arrays, {})
+ mock_post.return_value = mock_response
+
+ client = ForecastClient(base_url="https://api.example.com")
+ request = FlowStateForecastRequest(x=data, horizon=5, prediction_type="mean")
+
+ # Execute with warning capture
+ with pytest.warns(UserWarning, match="FlowState model only supports univariate forecasting"):
+ response = client.forecast(request)
+
+ # Verify output shape: should be reshaped to (2, 5, 3)
+ assert response.point is not None
+ assert response.point.shape == (2, 5, 3)
+
+ @patch("httpx.Client.post")
+ def test_tirex_multivariate_point_forecast(self, mock_post):
+ """Test TiRex with multivariate input transforms and reshapes correctly for point forecast."""
+ # Setup: multivariate input (3, 20, 2) - 3 series, 20 timesteps, 2 features
+ data = np.random.rand(3, 20, 2).astype(np.float32)
+
+ # Mock server response: (batch*features=6, horizon=10, features=1)
+ response_arrays = {"point": np.random.rand(6, 10, 1).astype(np.float32)}
+ mock_response = Mock()
+ mock_response.status_code = 200
+ mock_response.content = serialize_to_arrow(response_arrays, {})
+ mock_post.return_value = mock_response
+
+ client = ForecastClient(base_url="https://api.example.com")
+ request = TiRexForecastRequest(x=data, horizon=10)
+
+ # Execute with warning capture
+ with pytest.warns(UserWarning, match="TiRex model only supports univariate forecasting"):
+ response = client.forecast(request)
+
+ # Verify output shape: should be reshaped to (3, 10, 2)
+ assert response.point is not None
+ assert response.point.shape == (3, 10, 2)
+
+ @patch("httpx.Client.post")
+ def test_flowstate_multivariate_quantile_forecast(self, mock_post):
+ """Test FlowState with multivariate input transforms and reshapes correctly for quantile forecast."""
+ # Setup: multivariate input (2, 15, 4) - 2 series, 15 timesteps, 4 features
+ data = np.random.rand(2, 15, 4).astype(np.float32)
+
+ # Mock server response: (batch*features=8, horizon=8, quantiles=5, features=1)
+ response_arrays = {"quantiles": np.random.rand(8, 8, 5, 1).astype(np.float32)}
+ mock_response = Mock()
+ mock_response.status_code = 200
+ mock_response.content = serialize_to_arrow(response_arrays, {})
+ mock_post.return_value = mock_response
+
+ client = ForecastClient(base_url="https://api.example.com")
+ request = FlowStateForecastRequest(x=data, horizon=8, prediction_type="quantile", output_type="quantiles")
+
+ # Execute with warning capture
+ with pytest.warns(UserWarning, match="FlowState model only supports univariate forecasting"):
+ response = client.forecast(request)
+
+ # Verify output shape: should be reshaped to (2, 8, 5, 4)
+ assert response.quantiles is not None
+ assert response.quantiles.shape == (2, 8, 5, 4)
+
+ @patch("httpx.Client.post")
+ def test_tirex_multivariate_quantile_forecast(self, mock_post):
+ """Test TiRex with multivariate input transforms and reshapes correctly for quantile forecast."""
+ # Setup: multivariate input (1, 10, 3) - 1 series, 10 timesteps, 3 features
+ data = np.random.rand(1, 10, 3).astype(np.float32)
+
+ # Mock server response: (batch*features=3, horizon=5, quantiles=7, features=1)
+ response_arrays = {"quantiles": np.random.rand(3, 5, 7, 1).astype(np.float32)}
+ mock_response = Mock()
+ mock_response.status_code = 200
+ mock_response.content = serialize_to_arrow(response_arrays, {})
+ mock_post.return_value = mock_response
+
+ client = ForecastClient(base_url="https://api.example.com")
+ request = TiRexForecastRequest(x=data, horizon=5, output_type="quantiles")
+
+ # Execute with warning capture
+ with pytest.warns(UserWarning, match="TiRex model only supports univariate forecasting"):
+ response = client.forecast(request)
+
+ # Verify output shape: should be reshaped to (1, 5, 7, 3)
+ assert response.quantiles is not None
+ assert response.quantiles.shape == (1, 5, 7, 3)
+
+ @patch("httpx.Client.post")
+ def test_chronos2_multivariate_not_transformed(self, mock_post):
+ """Test that Chronos2 with multivariate input is NOT transformed."""
+ # Setup: multivariate input (2, 10, 3) - Chronos2 supports multivariate
+ data = np.random.rand(2, 10, 3).astype(np.float32)
+
+ # Mock server response: should preserve multivariate shape (2, 5, 3)
+ response_arrays = {"point": np.random.rand(2, 5, 3).astype(np.float32)}
+ mock_response = Mock()
+ mock_response.status_code = 200
+ mock_response.content = serialize_to_arrow(response_arrays, {})
+ mock_post.return_value = mock_response
+
+ client = ForecastClient(base_url="https://api.example.com")
+ request = Chronos2ForecastRequest(x=data, horizon=5)
+
+ # Execute - should NOT issue warning
+ with pytest.warns(None) as warning_list:
+ response = client.forecast(request)
+
+ # Verify no univariate transformation warning was issued
+ univariate_warnings = [w for w in warning_list if "univariate forecasting" in str(w.message)]
+ assert len(univariate_warnings) == 0
+
+ # Verify output shape: should remain (2, 5, 3)
+ assert response.point is not None
+ assert response.point.shape == (2, 5, 3)
+
+ @patch("httpx.Client.post")
+ def test_flowstate_univariate_not_transformed(self, mock_post):
+ """Test that FlowState with univariate input (features=1) is NOT transformed."""
+ # Setup: univariate input (2, 10, 1) - already univariate
+ data = np.random.rand(2, 10, 1).astype(np.float32)
+
+ # Mock server response: (2, 5, 1)
+ response_arrays = {"point": np.random.rand(2, 5, 1).astype(np.float32)}
+ mock_response = Mock()
+ mock_response.status_code = 200
+ mock_response.content = serialize_to_arrow(response_arrays, {})
+ mock_post.return_value = mock_response
+
+ client = ForecastClient(base_url="https://api.example.com")
+ request = FlowStateForecastRequest(x=data, horizon=5, prediction_type="mean")
+
+ # Execute - should NOT issue warning for univariate input
+ with pytest.warns(None) as warning_list:
+ response = client.forecast(request)
+
+ # Verify no univariate transformation warning was issued
+ univariate_warnings = [w for w in warning_list if "univariate forecasting" in str(w.message)]
+ assert len(univariate_warnings) == 0
+
+ # Verify output shape: should remain (2, 5, 1)
+ assert response.point is not None
+ assert response.point.shape == (2, 5, 1)
+
+ def test_2d_input_raises_error(self):
+ """Test that 2D input raises validation error."""
+ # Setup: 2D input (10, 3) - not allowed
+ data = np.random.rand(10, 3).astype(np.float32)
+
+ # Should raise error during request validation
+ with pytest.raises(ValueError, match="x must be a 3D array"):
+ FlowStateForecastRequest(x=data, horizon=5, prediction_type="mean")
+
+ @patch("httpx.Client.post")
+ @pytest.mark.asyncio
+ async def test_flowstate_multivariate_async(self, mock_post):
+ """Test async forecast with FlowState multivariate transformation."""
+ # Setup: multivariate input (2, 10, 3)
+ data = np.random.rand(2, 10, 3).astype(np.float32)
+
+ # Mock server response: (batch*features=6, horizon=5, features=1)
+ response_arrays = {"point": np.random.rand(6, 5, 1).astype(np.float32)}
+ mock_response = Mock()
+ mock_response.status_code = 200
+ mock_response.content = serialize_to_arrow(response_arrays, {})
+ mock_post.return_value = mock_response
+
+ client = ForecastClient(base_url="https://api.example.com")
+ request = FlowStateForecastRequest(x=data, horizon=5, prediction_type="mean")
+
+ # Execute async with warning capture
+ with pytest.warns(UserWarning, match="FlowState model only supports univariate forecasting"):
+ response = await client.forecast_async(request)
+
+ # Verify output shape: should be reshaped to (2, 5, 3)
+ assert response.point is not None
+ assert response.point.shape == (2, 5, 3)
+
+ @patch("httpx.Client.post")
+ def test_warning_message_content(self, mock_post):
+ """Test that warning message contains correct information."""
+ # Setup: multivariate input with 5 features
+ data = np.random.rand(1, 10, 5).astype(np.float32)
+
+ # Mock server response
+ response_arrays = {"point": np.random.rand(5, 5, 1).astype(np.float32)}
+ mock_response = Mock()
+ mock_response.status_code = 200
+ mock_response.content = serialize_to_arrow(response_arrays, {})
+ mock_post.return_value = mock_response
+
+ client = ForecastClient(base_url="https://api.example.com")
+ request = FlowStateForecastRequest(x=data, horizon=5, prediction_type="mean")
+
+ # Capture warning
+ with pytest.warns(UserWarning) as warning_record:
+ client.forecast(request)
+
+ # Verify warning message
+ assert len(warning_record) == 1
+ warning_message = str(warning_record[0].message)
+ assert "FlowState" in warning_message
+ assert "5 features" in warning_message
+ assert "independently" in warning_message
+ assert "separate time series" in warning_message
diff --git a/tests/unit/test_eval_metrics.py b/tests/unit/test_eval_metrics.py
new file mode 100644
index 0000000..2761727
--- /dev/null
+++ b/tests/unit/test_eval_metrics.py
@@ -0,0 +1,378 @@
+"""Unit tests for faim_sdk.eval.metrics module.
+
+Tests for MSE, MASE, and CRPS metrics with comprehensive coverage
+of functionality, edge cases, and error handling.
+"""
+
+import numpy as np
+import pytest
+
+from faim_sdk.eval.metrics import crps_from_quantiles, mase, mse
+
+
+class TestMSE:
+ """Tests for Mean Squared Error (MSE) metric."""
+
+ def test_mse_perfect_prediction(self):
+ """Test MSE returns 0 for perfect predictions."""
+ y_true = np.array([[[1.0], [2.0], [3.0]]])
+ y_pred = np.array([[[1.0], [2.0], [3.0]]])
+
+ result = mse(y_true, y_pred, reduction="mean")
+ assert result == 0.0
+
+ def test_mse_known_values(self):
+ """Test MSE with known input/output values."""
+ y_true = np.array([[[1.0], [2.0], [3.0]]])
+ y_pred = np.array([[[2.0], [3.0], [4.0]]])
+
+ # Expected: ((1-2)^2 + (2-3)^2 + (3-4)^2) / 3 = 3/3 = 1.0
+ result = mse(y_true, y_pred, reduction="mean")
+ assert result == 1.0
+
+ def test_mse_reduction_none(self):
+ """Test MSE with reduction='none' returns per-sample metrics."""
+ y_true = np.array([[[1.0], [2.0]], [[3.0], [4.0]], [[5.0], [6.0]]])
+ y_pred = np.array(
+ [
+ [[2.0], [3.0]], # Errors: 1, 1 -> MSE = 1.0
+ [[3.0], [4.0]], # Errors: 0, 0 -> MSE = 0.0
+ [[6.0], [8.0]], # Errors: 1, 2 -> MSE = (1+4)/2 = 2.5
+ ]
+ )
+
+ result = mse(y_true, y_pred, reduction="none")
+
+ assert result.shape == (3,)
+ np.testing.assert_array_almost_equal(result, [1.0, 0.0, 2.5])
+
+ def test_mse_multi_feature(self):
+ """Test MSE with multiple features."""
+ y_true = np.array([[[1.0, 2.0, 3.0]]]) # (1, 1, 3)
+ y_pred = np.array([[[2.0, 2.0, 2.0]]])
+
+ # Errors: (1-2)^2=1, (2-2)^2=0, (3-2)^2=1
+ # MSE = (1 + 0 + 1) / 3 = 0.667
+ result = mse(y_true, y_pred, reduction="mean")
+ assert pytest.approx(result, rel=1e-6) == 2.0 / 3.0
+
+ def test_mse_large_batch(self):
+ """Test MSE with realistic batch size."""
+ batch_size = 32
+ horizon = 24
+ features = 5
+
+ y_true = np.random.randn(batch_size, horizon, features)
+ y_pred = y_true + np.random.randn(batch_size, horizon, features) * 0.1
+
+ result = mse(y_true, y_pred, reduction="mean")
+ assert isinstance(result, float)
+ assert result > 0
+
+ def test_mse_wrong_shape_error(self):
+ """Test MSE raises error for mismatched shapes."""
+ y_true = np.array([[[1.0], [2.0]]])
+ y_pred = np.array([[[1.0]]]) # Wrong horizon
+
+ with pytest.raises(ValueError, match="must have the same shape"):
+ mse(y_true, y_pred)
+
+ def test_mse_wrong_dimensions_error(self):
+ """Test MSE raises error for wrong number of dimensions."""
+ y_true = np.array([[1.0, 2.0]]) # 2D instead of 3D
+ y_pred = np.array([[1.0, 2.0]])
+
+ with pytest.raises(ValueError, match="must be 3-dimensional"):
+ mse(y_true, y_pred)
+
+ def test_mse_type_error(self):
+ """Test MSE raises error for non-numpy inputs."""
+ y_true = [[1.0, 2.0]] # Python list
+ y_pred = np.array([[[1.0], [2.0]]])
+
+ with pytest.raises(TypeError, match="must be numpy.ndarray"):
+ mse(y_true, y_pred)
+
+ def test_mse_empty_array_error(self):
+ """Test MSE raises error for empty arrays."""
+ y_true = np.array([]).reshape(1, 0, 1)
+ y_pred = np.array([]).reshape(1, 0, 1)
+
+ with pytest.raises(ValueError, match="cannot be empty"):
+ mse(y_true, y_pred)
+
+ def test_mse_invalid_reduction_error(self):
+ """Test MSE raises error for invalid reduction parameter."""
+ y_true = np.array([[[1.0]]])
+ y_pred = np.array([[[1.0]]])
+
+ with pytest.raises(ValueError, match="reduction must be 'mean' or 'none'"):
+ mse(y_true, y_pred, reduction="invalid")
+
+
+class TestMASE:
+ """Tests for Mean Absolute Scaled Error (MASE) metric."""
+
+ def test_mase_perfect_prediction(self):
+ """Test MASE returns 0 for perfect predictions."""
+ y_train = np.array([[[1.0], [2.0], [3.0], [4.0]]])
+ y_true = np.array([[[5.0], [6.0]]])
+ y_pred = np.array([[[5.0], [6.0]]])
+
+ result = mase(y_true, y_pred, y_train, reduction="mean")
+ assert result == 0.0
+
+ def test_mase_known_values(self):
+ """Test MASE with known input/output values."""
+ # Training: [1, 2, 3, 4, 5] -> naive MAE = mean(|1|, |1|, |1|, |1|) = 1.0
+ y_train = np.array([[[1.0], [2.0], [3.0], [4.0], [5.0]]])
+
+ # Test: true=[6, 7, 8], pred=[6.1, 7.1, 8.1]
+ # Forecast MAE = mean(|0.1|, |0.1|, |0.1|) = 0.1
+ # MASE = 0.1 / 1.0 = 0.1
+ y_true = np.array([[[6.0], [7.0], [8.0]]])
+ y_pred = np.array([[[6.1], [7.1], [8.1]]])
+
+ result = mase(y_true, y_pred, y_train, reduction="mean")
+ assert pytest.approx(result, rel=1e-6) == 0.1
+
+ def test_mase_worse_than_naive(self):
+ """Test MASE > 1 when forecast is worse than naive baseline."""
+ # Training: constant changes of 1
+ y_train = np.array([[[1.0], [2.0], [3.0], [4.0]]])
+
+ # Bad forecast: errors of 2
+ y_true = np.array([[[5.0], [6.0]]])
+ y_pred = np.array([[[7.0], [8.0]]])
+
+ # Naive MAE = 1.0, Forecast MAE = 2.0, MASE = 2.0
+ result = mase(y_true, y_pred, y_train, reduction="mean")
+ assert result == 2.0
+
+ def test_mase_reduction_none(self):
+ """Test MASE with reduction='none' returns per-sample metrics."""
+ y_train = np.array([[[1.0], [2.0], [3.0]], [[2.0], [4.0], [6.0]], [[5.0], [5.0], [5.0]]])
+ y_true = np.array([[[4.0]], [[8.0]], [[5.0]]])
+ y_pred = np.array(
+ [
+ [[4.5]], # Error: 0.5, Naive MAE: 1.0 -> MASE: 0.5
+ [[9.0]], # Error: 1.0, Naive MAE: 2.0 -> MASE: 0.5
+ [[6.0]], # Error: 1.0, Naive MAE: 0.0 -> MASE: inf (constant)
+ ]
+ )
+
+ result = mase(y_true, y_pred, y_train, reduction="none")
+
+ assert result.shape == (3,)
+ # Third sample has constant training series, so MASE will be very high
+ assert result[0] == pytest.approx(0.5, rel=1e-6)
+ assert result[1] == pytest.approx(0.5, rel=1e-6)
+ # Third element will be inf or very large due to constant series
+
+ def test_mase_multi_feature(self):
+ """Test MASE with multiple features."""
+ y_train = np.array([[[1.0, 10.0], [2.0, 20.0], [3.0, 30.0]]])
+ y_true = np.array([[[4.0, 40.0]]])
+ y_pred = np.array([[[4.1, 40.5]]])
+
+ result = mase(y_true, y_pred, y_train, reduction="mean")
+ assert isinstance(result, float)
+ assert result > 0
+
+ def test_mase_constant_training_warning(self):
+ """Test MASE warns about constant training series."""
+ y_train = np.array([[[5.0], [5.0], [5.0]]]) # Constant
+ y_true = np.array([[[5.0]]])
+ y_pred = np.array([[[6.0]]])
+
+ with pytest.warns(RuntimeWarning, match="Naive baseline MAE is zero"):
+ result = mase(y_true, y_pred, y_train, reduction="mean")
+ # Should return large value due to epsilon
+ assert result > 0
+
+ def test_mase_shape_mismatch_error(self):
+ """Test MASE raises error for mismatched shapes."""
+ y_train = np.array([[[1.0], [2.0]]])
+ y_true = np.array([[[3.0]], [[4.0]]]) # Wrong batch size
+ y_pred = np.array([[[3.0]]])
+
+ with pytest.raises(ValueError, match="Batch size mismatch"):
+ mase(y_true, y_pred, y_train)
+
+ def test_mase_feature_mismatch_error(self):
+ """Test MASE raises error for mismatched features."""
+ y_train = np.array([[[1.0, 2.0], [3.0, 4.0]]])
+ y_true = np.array([[[5.0]]]) # Wrong features
+ y_pred = np.array([[[5.0]]])
+
+ with pytest.raises(ValueError, match="Feature count mismatch"):
+ mase(y_true, y_pred, y_train)
+
+ def test_mase_short_training_error(self):
+ """Test MASE raises error for training data that's too short."""
+ y_train = np.array([[[1.0]]]) # Only 1 time step
+ y_true = np.array([[[2.0]]])
+ y_pred = np.array([[[2.0]]])
+
+ with pytest.raises(ValueError, match="must have at least 2 time steps"):
+ mase(y_true, y_pred, y_train)
+
+ def test_mase_type_error(self):
+ """Test MASE raises error for non-numpy inputs."""
+ y_train = [[1.0, 2.0]]
+ y_true = np.array([[[3.0]]])
+ y_pred = np.array([[[3.0]]])
+
+ with pytest.raises(TypeError, match="must be numpy.ndarray"):
+ mase(y_true, y_pred, y_train)
+
+
+class TestCRPS:
+ """Tests for Continuous Ranked Probability Score (CRPS) metric."""
+
+ def test_crps_perfect_prediction(self):
+ """Test CRPS returns 0 for perfect predictions."""
+ y_true = np.array([[[5.0]]])
+ quantile_preds = np.array([[[4.5, 5.0, 5.5]]]) # 10th, 50th, 90th
+ quantile_levels = [0.1, 0.5, 0.9]
+
+ result = crps_from_quantiles(y_true, quantile_preds, quantile_levels, reduction="mean")
+ assert result == 0.0
+
+ def test_crps_known_values(self):
+ """Test CRPS with known input/output values."""
+ y_true = np.array([[[5.0]]])
+ quantile_preds = np.array([[[4.0, 5.0, 6.0]]])
+ quantile_levels = [0.1, 0.5, 0.9]
+
+ # Weights calculation:
+ # w[0] = 0.5 - 0.1 = 0.4
+ # w[1] = (0.9 - 0.1) / 2 = 0.4
+ # w[2] = 0.9 - 0.5 = 0.4
+ # Normalized: all 0.4 / 1.2 = 1/3
+
+ # Errors: |5-4| = 1, |5-5| = 0, |5-6| = 1
+ # CRPS = (1/3) * 1 + (1/3) * 0 + (1/3) * 1 = 2/3
+ result = crps_from_quantiles(y_true, quantile_preds, quantile_levels, reduction="mean")
+ assert pytest.approx(result, rel=1e-6) == 2.0 / 3.0
+
+ def test_crps_single_quantile_equals_mae(self):
+ """Test CRPS with single quantile equals MAE."""
+ y_true = np.array([[[5.0], [6.0]]])
+ quantile_preds = np.array([[[5.5], [6.5]]])
+ quantile_levels = [0.5]
+
+ # Should equal MAE: (|5-5.5| + |6-6.5|) / 2 = 0.5
+ result = crps_from_quantiles(y_true, quantile_preds, quantile_levels, reduction="mean")
+ assert pytest.approx(result, rel=1e-6) == 0.5
+
+ def test_crps_reduction_none(self):
+ """Test CRPS with reduction='none' returns per-sample metrics."""
+ y_true = np.array([[[5.0]], [[6.0]]])
+ quantile_preds = np.array(
+ [
+ [[5.0]], # Perfect prediction
+ [[7.0]], # Error of 1
+ ]
+ )
+ quantile_levels = [0.5]
+
+ result = crps_from_quantiles(y_true, quantile_preds, quantile_levels, reduction="none")
+
+ assert result.shape == (2,)
+ assert result[0] == 0.0
+ assert result[1] == 1.0
+
+ def test_crps_multiple_quantiles(self):
+ """Test CRPS with many quantiles."""
+ y_true = np.array([[[50.0]]])
+ quantile_preds = np.array([[[40.0, 45.0, 50.0, 55.0, 60.0]]])
+ quantile_levels = [0.05, 0.25, 0.5, 0.75, 0.95]
+
+ result = crps_from_quantiles(y_true, quantile_preds, quantile_levels, reduction="mean")
+
+ # Median is perfect, so CRPS should be positive but not too large
+ assert isinstance(result, float)
+ assert result > 0
+ assert result < 20 # Reasonable bound
+
+ def test_crps_batch_processing(self):
+ """Test CRPS with realistic batch size."""
+ batch_size = 32
+ horizon = 24
+ num_quantiles = 9
+
+ y_true = np.random.randn(batch_size, horizon, 1) * 10 + 50
+ quantile_preds = np.random.randn(batch_size, horizon, num_quantiles) * 10 + 50
+ quantile_levels = [0.1, 0.2, 0.3, 0.4, 0.5, 0.6, 0.7, 0.8, 0.9]
+
+ result = crps_from_quantiles(y_true, quantile_preds, quantile_levels, reduction="mean")
+
+ assert isinstance(result, float)
+ assert result > 0
+
+ def test_crps_shape_mismatch_error(self):
+ """Test CRPS raises error for mismatched shapes."""
+ y_true = np.array([[[5.0]]])
+ quantile_levels = [0.1, 0.5]
+
+ with pytest.raises(ValueError, match="Batch size mismatch"):
+ crps_from_quantiles(
+ y_true,
+ np.array([[[4.0, 5.0]], [[4.0, 5.0]]]), # Wrong batch
+ quantile_levels,
+ )
+
+ def test_crps_quantile_length_mismatch_error(self):
+ """Test CRPS raises error when quantile_levels length doesn't match."""
+ y_true = np.array([[[5.0]]])
+ quantile_preds = np.array([[[4.0, 5.0, 6.0]]])
+ quantile_levels = [0.1, 0.9] # Only 2, but preds has 3
+
+ with pytest.raises(ValueError, match="quantile_levels length.*must match"):
+ crps_from_quantiles(y_true, quantile_preds, quantile_levels)
+
+ def test_crps_invalid_quantile_levels_error(self):
+ """Test CRPS raises error for invalid quantile levels."""
+ y_true = np.array([[[5.0]]])
+ quantile_preds = np.array([[[4.0, 5.0]]])
+ quantile_levels = [0.1, 1.5] # 1.5 is invalid
+
+ with pytest.raises(ValueError, match="quantile_levels must be in"):
+ crps_from_quantiles(y_true, quantile_preds, quantile_levels)
+
+ def test_crps_unsorted_quantile_levels_error(self):
+ """Test CRPS raises error for unsorted quantile levels."""
+ y_true = np.array([[[5.0]]])
+ quantile_preds = np.array([[[4.0, 5.0]]])
+ quantile_levels = [0.9, 0.1] # Not sorted
+
+ with pytest.raises(ValueError, match="must be sorted in ascending order"):
+ crps_from_quantiles(y_true, quantile_preds, quantile_levels)
+
+ def test_crps_multi_feature_error(self):
+ """Test CRPS raises error for multi-feature with wrong dimensions."""
+ y_true = np.array([[[5.0, 6.0, 7.0]]]) # 3 features
+ quantile_preds = np.array([[[4.0, 5.0]]]) # 2 quantiles
+ quantile_levels = [0.1, 0.9]
+
+ with pytest.raises(ValueError, match="features.*should be 1 or match num_quantiles"):
+ crps_from_quantiles(y_true, quantile_preds, quantile_levels)
+
+ def test_crps_type_error_quantile_levels(self):
+ """Test CRPS raises error for wrong type of quantile_levels."""
+ y_true = np.array([[[5.0]]])
+ quantile_preds = np.array([[[4.0, 5.0]]])
+ quantile_levels = np.array([0.1, 0.9]) # Should be list, not array
+
+ with pytest.raises(TypeError, match="quantile_levels must be a list"):
+ crps_from_quantiles(y_true, quantile_preds, quantile_levels)
+
+ def test_crps_empty_array_error(self):
+ """Test CRPS raises error for empty arrays."""
+ y_true = np.array([]).reshape(1, 0, 1)
+ quantile_preds = np.array([]).reshape(1, 0, 3)
+ quantile_levels = [0.1, 0.5, 0.9]
+
+ with pytest.raises(ValueError, match="cannot be empty"):
+ crps_from_quantiles(y_true, quantile_preds, quantile_levels)
diff --git a/tests/unit/test_eval_visualization.py b/tests/unit/test_eval_visualization.py
new file mode 100644
index 0000000..5844207
--- /dev/null
+++ b/tests/unit/test_eval_visualization.py
@@ -0,0 +1,385 @@
+"""Unit tests for faim_sdk.eval.visualization module.
+
+Tests for plot_forecast with comprehensive coverage of functionality,
+edge cases, and error handling. Uses matplotlib mocking where appropriate.
+"""
+
+import numpy as np
+import pytest
+
+
+class TestPlotForecast:
+ """Tests for plot_forecast visualization function."""
+
+ @pytest.fixture(autouse=True)
+ def setup_matplotlib(self):
+ """Ensure matplotlib is available for tests."""
+ try:
+ import matplotlib
+
+ matplotlib.use("Agg") # Use non-interactive backend for testing
+ except ImportError:
+ pytest.skip("matplotlib not available")
+
+ def test_plot_forecast_basic(self):
+ """Test basic plot_forecast functionality."""
+ from faim_sdk.eval import plot_forecast
+
+ train_data = np.random.randn(100, 1)
+ forecast = np.random.randn(24, 1)
+
+ fig, ax = plot_forecast(train_data, forecast)
+
+ assert fig is not None
+ assert ax is not None
+ assert len(ax.lines) >= 2 # At least train and forecast lines
+
+ def test_plot_forecast_with_test_data(self):
+ """Test plot_forecast with optional test data."""
+ from faim_sdk.eval import plot_forecast
+
+ train_data = np.random.randn(100, 1)
+ forecast = np.random.randn(24, 1)
+ test_data = np.random.randn(24, 1)
+
+ fig, ax = plot_forecast(train_data, forecast, test_data)
+
+ assert fig is not None
+ assert len(ax.lines) >= 3 # Train, forecast, and test lines
+
+ def test_plot_forecast_single_feature_with_title(self):
+ """Test plot_forecast with custom title."""
+ from faim_sdk.eval import plot_forecast
+
+ train_data = np.random.randn(100, 1)
+ forecast = np.random.randn(24, 1)
+
+ fig, ax = plot_forecast(train_data, forecast, title="Test Forecast")
+
+ assert ax.get_title() == "Test Forecast"
+
+ def test_plot_forecast_multi_feature_same_plot(self):
+ """Test plot_forecast with multiple features on same plot."""
+ from faim_sdk.eval import plot_forecast
+
+ train_data = np.random.randn(100, 3)
+ forecast = np.random.randn(24, 3)
+ test_data = np.random.randn(24, 3)
+
+ fig, ax = plot_forecast(
+ train_data,
+ forecast,
+ test_data,
+ features_on_same_plot=True,
+ feature_names=["Feature A", "Feature B", "Feature C"],
+ )
+
+ assert fig is not None
+ # Should have 3 features ร 3 lines (train, forecast, test) = 9 lines + vertical line
+ assert len(ax.lines) >= 9
+
+ def test_plot_forecast_multi_feature_subplots(self):
+ """Test plot_forecast with multiple features as subplots."""
+ from faim_sdk.eval import plot_forecast
+
+ train_data = np.random.randn(100, 3)
+ forecast = np.random.randn(24, 3)
+
+ fig, axes = plot_forecast(
+ train_data, forecast, features_on_same_plot=False, feature_names=["Temp", "Humidity", "Pressure"]
+ )
+
+ assert fig is not None
+ assert len(axes) == 3 # One subplot per feature
+ assert axes[0].get_title() == "Temp"
+ assert axes[1].get_title() == "Humidity"
+ assert axes[2].get_title() == "Pressure"
+
+ def test_plot_forecast_custom_figsize(self):
+ """Test plot_forecast with custom figure size."""
+ from faim_sdk.eval import plot_forecast
+
+ train_data = np.random.randn(100, 1)
+ forecast = np.random.randn(24, 1)
+
+ fig, ax = plot_forecast(train_data, forecast, figsize=(15, 8))
+
+ assert fig.get_figwidth() == 15
+ assert fig.get_figheight() == 8
+
+ def test_plot_forecast_save_path(self, tmp_path):
+ """Test plot_forecast saves to file."""
+ from faim_sdk.eval import plot_forecast
+
+ train_data = np.random.randn(100, 1)
+ forecast = np.random.randn(24, 1)
+
+ save_path = tmp_path / "forecast.png"
+ fig, ax = plot_forecast(train_data, forecast, save_path=str(save_path))
+
+ assert save_path.exists()
+ assert save_path.stat().st_size > 0
+
+ def test_plot_forecast_default_feature_names(self):
+ """Test plot_forecast generates default feature names."""
+ from faim_sdk.eval import plot_forecast
+
+ train_data = np.random.randn(100, 3)
+ forecast = np.random.randn(24, 3)
+
+ fig, axes = plot_forecast(train_data, forecast, features_on_same_plot=False)
+
+ # Should use default names: Feature 1, Feature 2, Feature 3
+ assert axes[0].get_title() == "Feature 1"
+ assert axes[1].get_title() == "Feature 2"
+ assert axes[2].get_title() == "Feature 3"
+
+ def test_plot_forecast_single_feature_default_name(self):
+ """Test plot_forecast uses 'Series' for single feature."""
+ from faim_sdk.eval import plot_forecast
+
+ train_data = np.random.randn(100, 1)
+ forecast = np.random.randn(24, 1)
+
+ fig, axes = plot_forecast(train_data, forecast, features_on_same_plot=False)
+
+ # Should use 'Series' for single feature
+ assert axes[0].get_title() == "Series"
+
+ def test_plot_forecast_wrong_dimensions_train_error(self):
+ """Test plot_forecast raises error for wrong train_data dimensions."""
+ from faim_sdk.eval import plot_forecast
+
+ train_data = np.random.randn(32, 100, 1) # 3D instead of 2D
+ forecast = np.random.randn(24, 1)
+
+ with pytest.raises(ValueError, match="train_data must be 2-dimensional"):
+ plot_forecast(train_data, forecast)
+
+ def test_plot_forecast_wrong_dimensions_forecast_error(self):
+ """Test plot_forecast raises error for wrong forecast dimensions."""
+ from faim_sdk.eval import plot_forecast
+
+ train_data = np.random.randn(100, 1)
+ forecast = np.random.randn(32, 24, 1) # 3D instead of 2D
+
+ with pytest.raises(ValueError, match="forecast must be 2-dimensional"):
+ plot_forecast(train_data, forecast)
+
+ def test_plot_forecast_wrong_dimensions_test_error(self):
+ """Test plot_forecast raises error for wrong test_data dimensions."""
+ from faim_sdk.eval import plot_forecast
+
+ train_data = np.random.randn(100, 1)
+ forecast = np.random.randn(24, 1)
+ test_data = np.random.randn(32, 24, 1) # 3D instead of 2D
+
+ with pytest.raises(ValueError, match="test_data must be 2-dimensional"):
+ plot_forecast(train_data, forecast, test_data)
+
+ def test_plot_forecast_feature_mismatch_error(self):
+ """Test plot_forecast raises error for mismatched features."""
+ from faim_sdk.eval import plot_forecast
+
+ train_data = np.random.randn(100, 3)
+ forecast = np.random.randn(24, 2) # Wrong features
+
+ with pytest.raises(ValueError, match="Feature count mismatch"):
+ plot_forecast(train_data, forecast)
+
+ def test_plot_forecast_horizon_mismatch_error(self):
+ """Test plot_forecast raises error for mismatched horizons."""
+ from faim_sdk.eval import plot_forecast
+
+ train_data = np.random.randn(100, 1)
+ forecast = np.random.randn(24, 1)
+ test_data = np.random.randn(12, 1) # Wrong horizon
+
+ with pytest.raises(ValueError, match="Horizon mismatch"):
+ plot_forecast(train_data, forecast, test_data)
+
+ def test_plot_forecast_too_many_features_error(self):
+ """Test plot_forecast raises error for too many features on same plot."""
+ from faim_sdk.eval import plot_forecast
+
+ train_data = np.random.randn(100, 11) # 11 features
+ forecast = np.random.randn(24, 11)
+
+ with pytest.raises(ValueError, match="Cannot plot 11 features on same plot"):
+ plot_forecast(train_data, forecast, features_on_same_plot=True)
+
+ def test_plot_forecast_wrong_feature_names_length_error(self):
+ """Test plot_forecast raises error for wrong feature_names length."""
+ from faim_sdk.eval import plot_forecast
+
+ train_data = np.random.randn(100, 3)
+ forecast = np.random.randn(24, 3)
+
+ with pytest.raises(ValueError, match="feature_names length.*must match"):
+ plot_forecast(
+ train_data,
+ forecast,
+ feature_names=["A", "B"], # Only 2, but 3 features
+ )
+
+ def test_plot_forecast_type_error_train(self):
+ """Test plot_forecast raises error for non-numpy train_data."""
+ from faim_sdk.eval import plot_forecast
+
+ train_data = [[1.0, 2.0]] # Python list
+ forecast = np.random.randn(24, 1)
+
+ with pytest.raises(TypeError, match="train_data must be numpy.ndarray"):
+ plot_forecast(train_data, forecast)
+
+ def test_plot_forecast_type_error_forecast(self):
+ """Test plot_forecast raises error for non-numpy forecast."""
+ from faim_sdk.eval import plot_forecast
+
+ train_data = np.random.randn(100, 1)
+ forecast = [[1.0, 2.0]] # Python list
+
+ with pytest.raises(TypeError, match="forecast must be numpy.ndarray"):
+ plot_forecast(train_data, forecast)
+
+ def test_plot_forecast_type_error_test(self):
+ """Test plot_forecast raises error for non-numpy test_data."""
+ from faim_sdk.eval import plot_forecast
+
+ train_data = np.random.randn(100, 1)
+ forecast = np.random.randn(24, 1)
+ test_data = [[1.0, 2.0]] # Python list
+
+ with pytest.raises(TypeError, match="test_data must be numpy.ndarray or None"):
+ plot_forecast(train_data, forecast, test_data)
+
+ def test_plot_forecast_empty_train_error(self):
+ """Test plot_forecast raises error for empty train_data."""
+ from faim_sdk.eval import plot_forecast
+
+ train_data = np.array([]).reshape(0, 1)
+ forecast = np.random.randn(24, 1)
+
+ with pytest.raises(ValueError, match="train_data cannot be empty"):
+ plot_forecast(train_data, forecast)
+
+ def test_plot_forecast_empty_forecast_error(self):
+ """Test plot_forecast raises error for empty forecast."""
+ from faim_sdk.eval import plot_forecast
+
+ train_data = np.random.randn(100, 1)
+ forecast = np.array([]).reshape(0, 1)
+
+ with pytest.raises(ValueError, match="forecast cannot be empty"):
+ plot_forecast(train_data, forecast)
+
+ def test_plot_forecast_helpful_error_message_batch_dim(self):
+ """Test plot_forecast provides helpful error for batch dimension."""
+ from faim_sdk.eval import plot_forecast
+
+ train_data = np.random.randn(32, 100, 1) # User forgot to index [i]
+ forecast = np.random.randn(24, 1)
+
+ with pytest.raises(ValueError, match="Did you forget to index the batch dimension"):
+ plot_forecast(train_data, forecast)
+
+ def test_plot_forecast_matplotlib_not_available_error(self, monkeypatch):
+ """Test plot_forecast raises error when matplotlib not available."""
+ # This test simulates matplotlib not being installed
+ import sys
+
+ # Remove matplotlib from modules
+ matplotlib_modules = [key for key in sys.modules if key.startswith("matplotlib")]
+ for mod in matplotlib_modules:
+ monkeypatch.delitem(sys.modules, mod, raising=False)
+
+ # Mock import to fail
+ import builtins
+
+ original_import = builtins.__import__
+
+ def mock_import(name, *args, **kwargs):
+ if name.startswith("matplotlib"):
+ raise ImportError("No module named 'matplotlib'")
+ return original_import(name, *args, **kwargs)
+
+ monkeypatch.setattr(builtins, "__import__", mock_import)
+
+ # Force reload of visualization module
+ import importlib
+
+ import faim_sdk.eval
+
+ importlib.reload(faim_sdk.eval.visualization)
+
+ from faim_sdk.eval.visualization import plot_forecast
+
+ train_data = np.random.randn(100, 1)
+ forecast = np.random.randn(24, 1)
+
+ with pytest.raises(ImportError, match="matplotlib is required"):
+ plot_forecast(train_data, forecast)
+
+ def test_plot_forecast_legend_present(self):
+ """Test plot_forecast includes legend."""
+ from faim_sdk.eval import plot_forecast
+
+ train_data = np.random.randn(100, 1)
+ forecast = np.random.randn(24, 1)
+
+ fig, ax = plot_forecast(train_data, forecast)
+
+ legend = ax.get_legend()
+ assert legend is not None
+ assert len(legend.get_texts()) >= 2 # At least train and forecast
+
+ def test_plot_forecast_grid_present(self):
+ """Test plot_forecast includes grid."""
+ from faim_sdk.eval import plot_forecast
+
+ train_data = np.random.randn(100, 1)
+ forecast = np.random.randn(24, 1)
+
+ fig, ax = plot_forecast(train_data, forecast)
+
+ # Check if grid is enabled
+ assert ax.xaxis.grid or ax.yaxis.grid
+
+ def test_plot_forecast_vertical_line_at_boundary(self):
+ """Test plot_forecast includes vertical line at train/forecast boundary."""
+ from faim_sdk.eval import plot_forecast
+
+ train_data = np.random.randn(100, 1)
+ forecast = np.random.randn(24, 1)
+
+ fig, ax = plot_forecast(train_data, forecast)
+
+ # Check for vertical lines (boundary marker)
+ # Should have at least one vertical line for the boundary
+ # (Plus the actual data lines)
+ assert len(ax.lines) >= 2
+
+ def test_plot_forecast_realistic_sdk_usage(self):
+ """Test plot_forecast with realistic SDK response format."""
+ from faim_sdk.eval import plot_forecast
+
+ # Simulate SDK response
+ batch_size = 32
+ train_length = 100
+ horizon = 24
+ features = 1
+
+ train_batch = np.random.randn(batch_size, train_length, features)
+ forecast_batch = np.random.randn(batch_size, horizon, features)
+ test_batch = np.random.randn(batch_size, horizon, features)
+
+ # Plot single sample (index batch dimension)
+ fig, ax = plot_forecast(
+ train_data=train_batch[0], # (100, 1)
+ forecast=forecast_batch[0], # (24, 1)
+ test_data=test_batch[0], # (24, 1)
+ title="Sample 1 Forecast",
+ )
+
+ assert fig is not None
+ assert ax.get_title() == "Sample 1 Forecast"
diff --git a/tests/unit/test_exceptions.py b/tests/unit/test_exceptions.py
new file mode 100644
index 0000000..32f6d61
--- /dev/null
+++ b/tests/unit/test_exceptions.py
@@ -0,0 +1,628 @@
+"""Unit tests for faim_sdk.exceptions module.
+
+Tests exception hierarchy, error handling, and error contract integration.
+"""
+
+import pytest
+
+from faim_client.models.error_code import ErrorCode
+from faim_client.models.error_response import ErrorResponse
+from faim_sdk.exceptions import (
+ APIError,
+ AuthenticationError,
+ ConfigurationError,
+ FAIMError,
+ InsufficientFundsError,
+ InternalServerError,
+ ModelNotFoundError,
+ NetworkError,
+ PayloadTooLargeError,
+ RateLimitError,
+ SerializationError,
+ ServiceUnavailableError,
+ TimeoutError,
+ ValidationError,
+)
+
+
+class TestFAIMError:
+ """Tests for base FAIMError exception."""
+
+ def test_initialization_with_message(self):
+ """Test basic initialization with message."""
+ error = FAIMError("Something went wrong")
+ assert error.message == "Something went wrong"
+ assert error.details == {}
+
+ def test_initialization_with_details(self):
+ """Test initialization with details."""
+ details = {"key": "value", "count": 42}
+ error = FAIMError("Error occurred", details=details)
+
+ assert error.message == "Error occurred"
+ assert error.details == details
+ assert error.details["key"] == "value"
+ assert error.details["count"] == 42
+
+ def test_str_without_details(self):
+ """Test string representation without details."""
+ error = FAIMError("Test error")
+ assert str(error) == "Test error"
+
+ def test_str_with_details(self):
+ """Test string representation with details."""
+ error = FAIMError("Test error", details={"foo": "bar"})
+ result = str(error)
+
+ assert "Test error" in result
+ assert "details:" in result
+ assert "foo" in result
+
+ def test_inherits_from_exception(self):
+ """Test that FAIMError inherits from Exception."""
+ error = FAIMError("Test")
+ assert isinstance(error, Exception)
+
+ def test_can_be_raised_and_caught(self):
+ """Test that error can be raised and caught."""
+ with pytest.raises(FAIMError) as exc_info:
+ raise FAIMError("Test error")
+
+ assert exc_info.value.message == "Test error"
+
+
+class TestSerializationError:
+ """Tests for SerializationError exception."""
+
+ def test_inherits_from_faim_error(self):
+ """Test inheritance from FAIMError."""
+ error = SerializationError("Serialization failed")
+ assert isinstance(error, FAIMError)
+ assert isinstance(error, Exception)
+
+ def test_initialization(self):
+ """Test basic initialization."""
+ error = SerializationError("Arrow serialization failed")
+ assert error.message == "Arrow serialization failed"
+
+ def test_with_details(self):
+ """Test with additional details."""
+ error = SerializationError(
+ "Invalid array type",
+ details={"expected": "ndarray", "got": "list"},
+ )
+ assert error.details["expected"] == "ndarray"
+
+
+class TestAPIError:
+ """Tests for base APIError exception."""
+
+ def test_inherits_from_faim_error(self):
+ """Test inheritance from FAIMError."""
+ error = APIError("API failed")
+ assert isinstance(error, FAIMError)
+
+ def test_initialization_minimal(self):
+ """Test initialization with minimal parameters."""
+ error = APIError("Request failed")
+
+ assert error.message == "Request failed"
+ assert error.status_code is None
+ assert error.error_response is None
+ assert error.details == {}
+
+ def test_initialization_with_status_code(self):
+ """Test initialization with status code."""
+ error = APIError("Request failed", status_code=500)
+ assert error.status_code == 500
+
+ def test_initialization_with_error_response(self):
+ """Test initialization with ErrorResponse."""
+ err_response = ErrorResponse(
+ error_code=ErrorCode.VALIDATION_ERROR,
+ message="Validation failed",
+ detail="Invalid parameter",
+ request_id="req_123",
+ )
+ error = APIError("Request failed", error_response=err_response)
+
+ assert error.error_response == err_response
+ assert error.error_response.error_code == ErrorCode.VALIDATION_ERROR
+
+ def test_error_code_property_with_response(self):
+ """Test error_code property when error_response is available."""
+ err_response = ErrorResponse(
+ error_code=ErrorCode.INVALID_SHAPE,
+ message="Shape error",
+ )
+ error = APIError("Failed", error_response=err_response)
+
+ assert error.error_code == ErrorCode.INVALID_SHAPE
+
+ def test_error_code_property_without_response(self):
+ """Test error_code property when error_response is None."""
+ error = APIError("Failed")
+ assert error.error_code is None
+
+ def test_str_minimal(self):
+ """Test string representation with minimal info."""
+ error = APIError("Request failed")
+ assert str(error) == "Request failed"
+
+ def test_str_with_status_code(self):
+ """Test string representation with status code."""
+ error = APIError("Request failed", status_code=422)
+ result = str(error)
+
+ assert "Request failed" in result
+ assert "status=422" in result
+
+ def test_str_with_error_response(self):
+ """Test string representation with error response."""
+ err_response = ErrorResponse(
+ error_code=ErrorCode.VALIDATION_ERROR,
+ message="Validation failed",
+ request_id="req_abc123",
+ )
+ error = APIError("Request failed", status_code=422, error_response=err_response)
+ result = str(error)
+
+ assert "Request failed" in result
+ assert "status=422" in result
+ assert "error_code=validation_error" in result
+ assert "request_id=req_abc123" in result
+
+ def test_str_with_all_fields(self):
+ """Test string representation with all fields."""
+ err_response = ErrorResponse(
+ error_code=ErrorCode.INVALID_SHAPE,
+ message="Shape error",
+ detail="Expected (32, 100, 1)",
+ request_id="req_xyz789",
+ )
+ error = APIError(
+ "Request failed",
+ status_code=422,
+ error_response=err_response,
+ details={"retry": True},
+ )
+ result = str(error)
+
+ assert "Request failed" in result
+ assert "status=422" in result
+ assert "error_code=invalid_shape" in result
+ assert "request_id=req_xyz789" in result
+ assert "details=" in result
+
+
+class TestValidationError:
+ """Tests for ValidationError exception."""
+
+ def test_inherits_from_api_error(self):
+ """Test inheritance hierarchy."""
+ error = ValidationError("Validation failed")
+ assert isinstance(error, APIError)
+ assert isinstance(error, FAIMError)
+
+ def test_with_error_code(self):
+ """Test with specific error code."""
+ err_response = ErrorResponse(
+ error_code=ErrorCode.INVALID_SHAPE,
+ message="Shape mismatch",
+ detail="Expected (batch, seq, feat)",
+ )
+ error = ValidationError(
+ "Validation failed",
+ status_code=422,
+ error_response=err_response,
+ )
+
+ assert error.error_code == ErrorCode.INVALID_SHAPE
+ assert error.status_code == 422
+
+
+class TestAuthenticationError:
+ """Tests for AuthenticationError exception."""
+
+ def test_inherits_from_api_error(self):
+ """Test inheritance hierarchy."""
+ error = AuthenticationError("Auth failed")
+ assert isinstance(error, APIError)
+
+ def test_typical_usage(self):
+ """Test typical authentication error scenario."""
+ err_response = ErrorResponse(
+ error_code=ErrorCode.INVALID_API_KEY,
+ message="API key is invalid",
+ request_id="req_auth_001",
+ )
+ error = AuthenticationError(
+ "Authentication failed",
+ status_code=401,
+ error_response=err_response,
+ )
+
+ assert error.error_code == ErrorCode.INVALID_API_KEY
+ assert error.status_code == 401
+
+
+class TestInsufficientFundsError:
+ """Tests for InsufficientFundsError exception."""
+
+ def test_inherits_from_api_error(self):
+ """Test inheritance hierarchy."""
+ error = InsufficientFundsError("Insufficient funds")
+ assert isinstance(error, APIError)
+
+ def test_typical_usage(self):
+ """Test typical billing error scenario."""
+ err_response = ErrorResponse(
+ error_code=ErrorCode.INSUFFICIENT_FUNDS,
+ message="Account balance too low",
+ detail="Required: $10, Available: $5",
+ )
+ error = InsufficientFundsError(
+ "Billing error",
+ status_code=402,
+ error_response=err_response,
+ )
+
+ assert error.error_code == ErrorCode.INSUFFICIENT_FUNDS
+ assert error.status_code == 402
+
+
+class TestRateLimitError:
+ """Tests for RateLimitError exception."""
+
+ def test_inherits_from_api_error(self):
+ """Test inheritance hierarchy."""
+ error = RateLimitError("Rate limit exceeded")
+ assert isinstance(error, APIError)
+
+ def test_typical_usage(self):
+ """Test typical rate limit scenario."""
+ err_response = ErrorResponse(
+ error_code=ErrorCode.RATE_LIMIT_EXCEEDED,
+ message="Too many requests",
+ detail="Limit: 100/min",
+ )
+ error = RateLimitError(
+ "Rate limit exceeded",
+ status_code=429,
+ error_response=err_response,
+ )
+
+ assert error.error_code == ErrorCode.RATE_LIMIT_EXCEEDED
+ assert error.status_code == 429
+
+
+class TestModelNotFoundError:
+ """Tests for ModelNotFoundError exception."""
+
+ def test_inherits_from_api_error(self):
+ """Test inheritance hierarchy."""
+ error = ModelNotFoundError("Model not found")
+ assert isinstance(error, APIError)
+
+ def test_typical_usage(self):
+ """Test typical model not found scenario."""
+ err_response = ErrorResponse(
+ error_code=ErrorCode.MODEL_NOT_FOUND,
+ message="Model version not available",
+ detail="chronos2:2.0 not found",
+ )
+ error = ModelNotFoundError(
+ "Model not found",
+ status_code=404,
+ error_response=err_response,
+ )
+
+ assert error.error_code == ErrorCode.MODEL_NOT_FOUND
+ assert error.status_code == 404
+
+
+class TestPayloadTooLargeError:
+ """Tests for PayloadTooLargeError exception."""
+
+ def test_inherits_from_api_error(self):
+ """Test inheritance hierarchy."""
+ error = PayloadTooLargeError("Payload too large")
+ assert isinstance(error, APIError)
+
+ def test_typical_usage(self):
+ """Test typical payload size error scenario."""
+ err_response = ErrorResponse(
+ error_code=ErrorCode.PAYLOAD_TOO_LARGE,
+ message="Request size exceeds limit",
+ detail="Size: 150MB, Limit: 100MB",
+ )
+ error = PayloadTooLargeError(
+ "Payload too large",
+ status_code=413,
+ error_response=err_response,
+ )
+
+ assert error.error_code == ErrorCode.PAYLOAD_TOO_LARGE
+ assert error.status_code == 413
+
+
+class TestInternalServerError:
+ """Tests for InternalServerError exception."""
+
+ def test_inherits_from_api_error(self):
+ """Test inheritance hierarchy."""
+ error = InternalServerError("Internal error")
+ assert isinstance(error, APIError)
+
+ def test_typical_usage(self):
+ """Test typical internal server error scenario."""
+ err_response = ErrorResponse(
+ error_code=ErrorCode.INTERNAL_ERROR,
+ message="An unexpected error occurred",
+ request_id="req_500_abc",
+ )
+ error = InternalServerError(
+ "Internal server error",
+ status_code=500,
+ error_response=err_response,
+ )
+
+ assert error.error_code == ErrorCode.INTERNAL_ERROR
+ assert error.status_code == 500
+
+
+class TestServiceUnavailableError:
+ """Tests for ServiceUnavailableError exception."""
+
+ def test_inherits_from_api_error(self):
+ """Test inheritance hierarchy."""
+ error = ServiceUnavailableError("Service unavailable")
+ assert isinstance(error, APIError)
+
+ def test_typical_usage_503(self):
+ """Test typical service unavailable scenario (503)."""
+ err_response = ErrorResponse(
+ error_code=ErrorCode.TRITON_CONNECTION_ERROR,
+ message="Cannot connect to inference backend",
+ )
+ error = ServiceUnavailableError(
+ "Service unavailable",
+ status_code=503,
+ error_response=err_response,
+ )
+
+ assert error.error_code == ErrorCode.TRITON_CONNECTION_ERROR
+ assert error.status_code == 503
+
+ def test_typical_usage_504(self):
+ """Test typical timeout scenario (504)."""
+ err_response = ErrorResponse(
+ error_code=ErrorCode.TIMEOUT_ERROR,
+ message="Request timeout",
+ )
+ error = ServiceUnavailableError(
+ "Gateway timeout",
+ status_code=504,
+ error_response=err_response,
+ )
+
+ assert error.error_code == ErrorCode.TIMEOUT_ERROR
+ assert error.status_code == 504
+
+
+class TestNetworkError:
+ """Tests for NetworkError exception."""
+
+ def test_inherits_from_faim_error(self):
+ """Test inheritance hierarchy."""
+ error = NetworkError("Network failed")
+ assert isinstance(error, FAIMError)
+ assert not isinstance(error, APIError)
+
+ def test_initialization(self):
+ """Test basic initialization."""
+ error = NetworkError("Connection refused")
+ assert error.message == "Connection refused"
+
+ def test_with_details(self):
+ """Test with connection details."""
+ error = NetworkError(
+ "Connection failed",
+ details={"host": "api.example.com", "port": 443},
+ )
+ assert error.details["host"] == "https://api.faim.it.com"
+
+
+class TestTimeoutError:
+ """Tests for TimeoutError exception."""
+
+ def test_inherits_from_faim_error(self):
+ """Test inheritance hierarchy."""
+ error = TimeoutError("Request timeout")
+ assert isinstance(error, FAIMError)
+ assert not isinstance(error, APIError)
+
+ def test_initialization(self):
+ """Test basic initialization."""
+ error = TimeoutError("Request exceeded 120s timeout")
+ assert error.message == "Request exceeded 120s timeout"
+
+ def test_with_details(self):
+ """Test with timeout details."""
+ error = TimeoutError(
+ "Timeout",
+ details={"configured": 120, "elapsed": 125},
+ )
+ assert error.details["configured"] == 120
+ assert error.details["elapsed"] == 125
+
+
+class TestConfigurationError:
+ """Tests for ConfigurationError exception."""
+
+ def test_inherits_from_faim_error(self):
+ """Test inheritance hierarchy."""
+ error = ConfigurationError("Config invalid")
+ assert isinstance(error, FAIMError)
+ assert not isinstance(error, APIError)
+
+ def test_initialization(self):
+ """Test basic initialization."""
+ error = ConfigurationError("Missing base_url")
+ assert error.message == "Missing base_url"
+
+ def test_with_details(self):
+ """Test with configuration details."""
+ error = ConfigurationError(
+ "Invalid configuration",
+ details={"field": "base_url", "value": None},
+ )
+ assert error.details["field"] == "base_url"
+
+
+class TestExceptionHierarchy:
+ """Tests for exception hierarchy and inheritance."""
+
+ def test_all_exceptions_inherit_from_faim_error(self):
+ """Test that all SDK exceptions inherit from FAIMError."""
+ exceptions = [
+ SerializationError("test"),
+ APIError("test"),
+ ValidationError("test"),
+ AuthenticationError("test"),
+ InsufficientFundsError("test"),
+ RateLimitError("test"),
+ ModelNotFoundError("test"),
+ PayloadTooLargeError("test"),
+ InternalServerError("test"),
+ ServiceUnavailableError("test"),
+ NetworkError("test"),
+ TimeoutError("test"),
+ ConfigurationError("test"),
+ ]
+
+ for exc in exceptions:
+ assert isinstance(exc, FAIMError)
+ assert isinstance(exc, Exception)
+
+ def test_api_errors_inherit_from_api_error(self):
+ """Test that API exceptions inherit from APIError."""
+ api_exceptions = [
+ ValidationError("test"),
+ AuthenticationError("test"),
+ InsufficientFundsError("test"),
+ RateLimitError("test"),
+ ModelNotFoundError("test"),
+ PayloadTooLargeError("test"),
+ InternalServerError("test"),
+ ServiceUnavailableError("test"),
+ ]
+
+ for exc in api_exceptions:
+ assert isinstance(exc, APIError)
+
+ def test_non_api_errors_dont_inherit_from_api_error(self):
+ """Test that non-API exceptions don't inherit from APIError."""
+ non_api_exceptions = [
+ SerializationError("test"),
+ NetworkError("test"),
+ TimeoutError("test"),
+ ConfigurationError("test"),
+ ]
+
+ for exc in non_api_exceptions:
+ assert not isinstance(exc, APIError)
+
+ def test_catch_all_with_faim_error(self):
+ """Test that FAIMError can catch all SDK exceptions."""
+ test_exceptions = [
+ ValidationError("test"),
+ NetworkError("test"),
+ SerializationError("test"),
+ ]
+
+ for exc_class in test_exceptions:
+ with pytest.raises(FAIMError):
+ raise exc_class
+
+ def test_catch_api_errors_specifically(self):
+ """Test catching only API errors."""
+ with pytest.raises(APIError):
+ raise ValidationError("test")
+
+ # Non-API errors should not be caught
+ with pytest.raises(FAIMError):
+ try:
+ raise NetworkError("test")
+ except APIError:
+ pytest.fail("NetworkError should not be caught as APIError")
+
+
+class TestErrorContract:
+ """Tests for error contract integration with ErrorResponse and ErrorCode."""
+
+ def test_error_response_integration(self):
+ """Test integration with ErrorResponse model."""
+ err_response = ErrorResponse(
+ error_code=ErrorCode.VALIDATION_ERROR,
+ message="Validation failed",
+ detail="horizon must be positive",
+ request_id="req_123",
+ metadata={"field": "horizon", "value": -5},
+ )
+
+ error = ValidationError("Request validation failed", error_response=err_response)
+
+ assert error.error_response.error_code == ErrorCode.VALIDATION_ERROR
+ assert error.error_response.message == "Validation failed"
+ assert error.error_response.detail == "horizon must be positive"
+ assert error.error_response.request_id == "req_123"
+ assert error.error_response.metadata["field"] == "horizon"
+
+ def test_error_code_enum_access(self):
+ """Test accessing ErrorCode enum through exception."""
+ err_response = ErrorResponse(
+ error_code=ErrorCode.INVALID_SHAPE,
+ message="Shape error",
+ )
+ error = ValidationError("Validation failed", error_response=err_response)
+
+ # Can access via property
+ assert error.error_code == ErrorCode.INVALID_SHAPE
+
+ # Can compare with enum values
+ assert error.error_code == ErrorCode.INVALID_SHAPE
+ assert error.error_code != ErrorCode.MISSING_REQUIRED_FIELD
+
+ def test_programmatic_error_handling(self):
+ """Test programmatic error handling with error codes."""
+
+ def handle_error(exc: APIError) -> str:
+ """Example error handler using error codes."""
+ if exc.error_code == ErrorCode.INVALID_SHAPE:
+ return "shape_error"
+ elif exc.error_code == ErrorCode.MISSING_REQUIRED_FIELD:
+ return "missing_field"
+ elif exc.error_code == ErrorCode.RATE_LIMIT_EXCEEDED:
+ return "rate_limit"
+ else:
+ return "unknown"
+
+ # Test different error codes
+ err1 = ValidationError(
+ "Failed",
+ error_response=ErrorResponse(error_code=ErrorCode.INVALID_SHAPE, message="Shape error"),
+ )
+ assert handle_error(err1) == "shape_error"
+
+ err2 = ValidationError(
+ "Failed",
+ error_response=ErrorResponse(error_code=ErrorCode.MISSING_REQUIRED_FIELD, message="Missing field"),
+ )
+ assert handle_error(err2) == "missing_field"
+
+ err3 = RateLimitError(
+ "Failed",
+ error_response=ErrorResponse(error_code=ErrorCode.RATE_LIMIT_EXCEEDED, message="Rate limit"),
+ )
+ assert handle_error(err3) == "rate_limit"
diff --git a/tests/unit/test_models.py b/tests/unit/test_models.py
new file mode 100644
index 0000000..40903e3
--- /dev/null
+++ b/tests/unit/test_models.py
@@ -0,0 +1,476 @@
+"""Unit tests for faim_sdk.models module.
+
+Tests request and response model validation, serialization, and type safety.
+"""
+
+import numpy as np
+import pytest
+
+from faim_client.models import ModelName
+from faim_sdk.models import (
+ Chronos2ForecastRequest,
+ FlowStateForecastRequest,
+ ForecastRequest,
+ ForecastResponse,
+ TiRexForecastRequest,
+)
+
+
+class TestForecastRequest:
+ """Tests for base ForecastRequest class."""
+
+ def test_cannot_instantiate_base_class_without_model_name(self):
+ """Base class requires _model_name to be defined."""
+ with pytest.raises(AttributeError):
+ ForecastRequest(
+ x=np.array([[1.0, 2.0], [3.0, 4.0]]),
+ horizon=10,
+ )
+
+ def test_validation_requires_numpy_array(self):
+ """x parameter must be numpy array."""
+ with pytest.raises(TypeError, match="x must be numpy.ndarray"):
+ Chronos2ForecastRequest(
+ x=[[1.0, 2.0], [3.0, 4.0]], # Python list, not ndarray
+ horizon=10,
+ )
+
+ def test_validation_requires_non_empty_array(self):
+ """x parameter cannot be empty."""
+ with pytest.raises(ValueError, match="x cannot be empty"):
+ Chronos2ForecastRequest(
+ x=np.array([]),
+ horizon=10,
+ )
+
+ def test_validation_requires_positive_horizon(self):
+ """horizon must be positive."""
+ with pytest.raises(ValueError, match="horizon must be positive"):
+ Chronos2ForecastRequest(
+ x=np.array([[1.0, 2.0]]),
+ horizon=0,
+ )
+
+ def test_validation_requires_positive_horizon_negative(self):
+ """horizon cannot be negative."""
+ with pytest.raises(ValueError, match="horizon must be positive"):
+ Chronos2ForecastRequest(
+ x=np.array([[1.0, 2.0]]),
+ horizon=-5,
+ )
+
+
+class TestChronos2ForecastRequest:
+ """Tests for Chronos2ForecastRequest model."""
+
+ def test_model_name_is_chronos2(self):
+ """Model name should be CHRONOS2."""
+ request = Chronos2ForecastRequest(
+ x=np.array([[1.0, 2.0]]),
+ horizon=10,
+ )
+ assert request.model_name == ModelName.CHRONOS2
+
+ def test_default_values(self):
+ """Test default parameter values."""
+ request = Chronos2ForecastRequest(
+ x=np.array([[1.0, 2.0]]),
+ horizon=10,
+ )
+ assert request.model_version == "1"
+ assert request.compression == "zstd"
+ assert request.output_type == "point"
+ assert request.quantiles is None
+
+ def test_custom_values(self):
+ """Test custom parameter values."""
+ data = np.random.rand(32, 100, 1)
+ request = Chronos2ForecastRequest(
+ x=data,
+ horizon=24,
+ model_version="2.0",
+ compression="lz4",
+ output_type="quantiles",
+ quantiles=[0.1, 0.5, 0.9],
+ )
+ assert request.horizon == 24
+ assert request.model_version == "2.0"
+ assert request.compression == "lz4"
+ assert request.output_type == "quantiles"
+ assert request.quantiles == [0.1, 0.5, 0.9]
+
+ def test_quantiles_validation_requires_range_0_to_1(self):
+ """Quantiles must be in [0.0, 1.0]."""
+ with pytest.raises(ValueError, match="quantiles must be in"):
+ Chronos2ForecastRequest(
+ x=np.array([[1.0, 2.0]]),
+ horizon=10,
+ quantiles=[0.1, 0.5, 1.5], # 1.5 is invalid
+ )
+
+ def test_quantiles_validation_negative_values(self):
+ """Quantiles cannot be negative."""
+ with pytest.raises(ValueError, match="quantiles must be in"):
+ Chronos2ForecastRequest(
+ x=np.array([[1.0, 2.0]]),
+ horizon=10,
+ quantiles=[-0.1, 0.5, 0.9],
+ )
+
+ def test_to_arrays_and_metadata(self):
+ """Test conversion to Arrow format."""
+ data = np.random.rand(32, 100, 1)
+ request = Chronos2ForecastRequest(
+ x=data,
+ horizon=24,
+ output_type="quantiles",
+ quantiles=[0.1, 0.5, 0.9],
+ )
+ arrays, metadata = request.to_arrays_and_metadata()
+
+ # Check arrays
+ assert "x" in arrays
+ assert np.array_equal(arrays["x"], data)
+
+ # Check metadata
+ assert metadata["horizon"] == 24
+ assert metadata["output_type"] == "quantiles"
+ assert metadata["quantiles"] == [0.1, 0.5, 0.9]
+
+ def test_to_arrays_and_metadata_without_quantiles(self):
+ """Test conversion when quantiles not specified."""
+ data = np.array([[1.0, 2.0]])
+ request = Chronos2ForecastRequest(
+ x=data,
+ horizon=10,
+ output_type="point",
+ )
+ arrays, metadata = request.to_arrays_and_metadata()
+
+ assert metadata["output_type"] == "point"
+ assert "quantiles" not in metadata
+
+
+class TestTiRexForecastRequest:
+ """Tests for TiRexForecastRequest model."""
+
+ def test_model_name_is_tirex(self):
+ """Model name should be TIREX."""
+ request = TiRexForecastRequest(
+ x=np.array([[1.0, 2.0]]),
+ horizon=10,
+ )
+ assert request.model_name == ModelName.TIREX
+
+ def test_default_values(self):
+ """Test default parameter values."""
+ request = TiRexForecastRequest(
+ x=np.array([[1.0, 2.0]]),
+ horizon=10,
+ )
+ assert request.model_version == "1"
+ assert request.compression == "zstd"
+ assert request.output_type == "point"
+
+ def test_custom_output_type(self):
+ """Test custom output type."""
+ request = TiRexForecastRequest(
+ x=np.array([[1.0, 2.0]]),
+ horizon=10,
+ output_type="quantiles",
+ )
+ assert request.output_type == "quantiles"
+
+ def test_to_arrays_and_metadata(self):
+ """Test conversion to Arrow format."""
+ data = np.random.rand(10, 50, 2)
+ request = TiRexForecastRequest(
+ x=data,
+ horizon=12,
+ output_type="samples",
+ )
+ arrays, metadata = request.to_arrays_and_metadata()
+
+ assert "x" in arrays
+ assert np.array_equal(arrays["x"], data)
+ assert metadata["horizon"] == 12
+ assert metadata["output_type"] == "samples"
+
+
+class TestFlowStateForecastRequest:
+ """Tests for FlowStateForecastRequest model."""
+
+ def test_model_name_is_flowstate(self):
+ """Model name should be FLOWSTATE."""
+ request = FlowStateForecastRequest(
+ x=np.array([[1.0, 2.0]]),
+ horizon=10,
+ )
+ assert request.model_name == ModelName.FLOWSTATE
+
+ def test_default_values(self):
+ """Test default parameter values."""
+ request = FlowStateForecastRequest(
+ x=np.array([[1.0, 2.0]]),
+ horizon=10,
+ )
+ assert request.model_version == "1"
+ assert request.compression == "zstd"
+ assert request.output_type == "point"
+ assert request.scale_factor is None
+ assert request.prediction_type is None
+
+ def test_custom_scale_factor(self):
+ """Test custom scale factor."""
+ request = FlowStateForecastRequest(
+ x=np.array([[1.0, 2.0]]),
+ horizon=10,
+ scale_factor=100.0,
+ )
+ assert request.scale_factor == 100.0
+
+ def test_scale_factor_validation_positive(self):
+ """Scale factor must be positive."""
+ with pytest.raises(ValueError, match="scale_factor must be positive"):
+ FlowStateForecastRequest(
+ x=np.array([[1.0, 2.0]]),
+ horizon=10,
+ scale_factor=0.0,
+ )
+
+ def test_scale_factor_validation_negative(self):
+ """Scale factor cannot be negative."""
+ with pytest.raises(ValueError, match="scale_factor must be positive"):
+ FlowStateForecastRequest(
+ x=np.array([[1.0, 2.0]]),
+ horizon=10,
+ scale_factor=-1.0,
+ )
+
+ def test_prediction_type_mean_with_point_output(self):
+ """prediction_type='mean' requires output_type='point'."""
+ request = FlowStateForecastRequest(
+ x=np.array([[1.0, 2.0]]),
+ horizon=10,
+ output_type="point",
+ prediction_type="mean",
+ )
+ assert request.prediction_type == "mean"
+ assert request.output_type == "point"
+
+ def test_prediction_type_median_with_point_output(self):
+ """prediction_type='median' requires output_type='point'."""
+ request = FlowStateForecastRequest(
+ x=np.array([[1.0, 2.0]]),
+ horizon=10,
+ output_type="point",
+ prediction_type="median",
+ )
+ assert request.prediction_type == "median"
+
+ def test_prediction_type_quantile_with_quantiles_output(self):
+ """prediction_type='quantile' requires output_type='quantiles'."""
+ request = FlowStateForecastRequest(
+ x=np.array([[1.0, 2.0]]),
+ horizon=10,
+ output_type="quantiles",
+ prediction_type="quantile",
+ )
+ assert request.prediction_type == "quantile"
+ assert request.output_type == "quantiles"
+
+ def test_validation_mean_requires_point_output(self):
+ """prediction_type='mean' incompatible with output_type='quantiles'."""
+ with pytest.raises(ValueError, match="prediction_type='mean' requires output_type='point'"):
+ FlowStateForecastRequest(
+ x=np.array([[1.0, 2.0]]),
+ horizon=10,
+ output_type="quantiles",
+ prediction_type="mean",
+ )
+
+ def test_validation_median_requires_point_output(self):
+ """prediction_type='median' incompatible with output_type='quantiles'."""
+ with pytest.raises(ValueError, match="prediction_type='median' requires output_type='point'"):
+ FlowStateForecastRequest(
+ x=np.array([[1.0, 2.0]]),
+ horizon=10,
+ output_type="quantiles",
+ prediction_type="median",
+ )
+
+ def test_validation_quantile_requires_quantiles_output(self):
+ """prediction_type='quantile' requires output_type='quantiles'."""
+ with pytest.raises(ValueError, match="prediction_type='quantile' requires output_type='quantiles'"):
+ FlowStateForecastRequest(
+ x=np.array([[1.0, 2.0]]),
+ horizon=10,
+ output_type="point",
+ prediction_type="quantile",
+ )
+
+ def test_validation_quantiles_output_requires_quantile_prediction(self):
+ """output_type='quantiles' requires prediction_type='quantile'."""
+ with pytest.raises(ValueError, match="output_type='quantiles' requires prediction_type='quantile'"):
+ FlowStateForecastRequest(
+ x=np.array([[1.0, 2.0]]),
+ horizon=10,
+ output_type="quantiles",
+ prediction_type="mean",
+ )
+
+ def test_validation_quantiles_output_requires_prediction_type(self):
+ """output_type='quantiles' requires prediction_type to be set."""
+ with pytest.raises(ValueError, match="output_type='quantiles' requires prediction_type='quantile'"):
+ FlowStateForecastRequest(
+ x=np.array([[1.0, 2.0]]),
+ horizon=10,
+ output_type="quantiles",
+ )
+
+ def test_to_arrays_and_metadata_with_all_params(self):
+ """Test conversion with all FlowState parameters."""
+ data = np.random.rand(16, 80, 1)
+ request = FlowStateForecastRequest(
+ x=data,
+ horizon=20,
+ scale_factor=50.0,
+ prediction_type="mean",
+ output_type="point",
+ )
+ arrays, metadata = request.to_arrays_and_metadata()
+
+ assert "x" in arrays
+ assert np.array_equal(arrays["x"], data)
+ assert metadata["horizon"] == 20
+ assert metadata["output_type"] == "point"
+ assert metadata["scale_factor"] == 50.0
+ assert metadata["prediction_type"] == "mean"
+
+ def test_to_arrays_and_metadata_without_optional_params(self):
+ """Test conversion without optional FlowState parameters."""
+ data = np.array([[1.0, 2.0]])
+ request = FlowStateForecastRequest(
+ x=data,
+ horizon=10,
+ )
+ arrays, metadata = request.to_arrays_and_metadata()
+
+ assert metadata["output_type"] == "point"
+ assert "scale_factor" not in metadata
+ assert "prediction_type" not in metadata
+
+
+class TestForecastResponse:
+ """Tests for ForecastResponse model."""
+
+ def test_from_arrays_point_only(self):
+ """Test creating response with point predictions only."""
+ point_data = np.random.rand(32, 24, 1)
+ arrays = {"point": point_data}
+ metadata = {"model_name": "chronos2", "model_version": "1.0"}
+
+ response = ForecastResponse.from_arrays_and_metadata(arrays, metadata)
+
+ assert response.point is not None
+ assert np.array_equal(response.point, point_data)
+ assert response.quantiles is None
+ assert response.samples is None
+ assert response.metadata == metadata
+
+ def test_from_arrays_quantiles_only(self):
+ """Test creating response with quantile predictions only."""
+ quantiles_data = np.random.rand(32, 24, 3)
+ arrays = {"quantiles": quantiles_data}
+ metadata = {"model_name": "chronos2", "quantiles": [0.1, 0.5, 0.9]}
+
+ response = ForecastResponse.from_arrays_and_metadata(arrays, metadata)
+
+ assert response.point is None
+ assert response.quantiles is not None
+ assert np.array_equal(response.quantiles, quantiles_data)
+ assert response.samples is None
+
+ def test_from_arrays_samples_only(self):
+ """Test creating response with sample predictions only."""
+ samples_data = np.random.rand(32, 24, 100)
+ arrays = {"samples": samples_data}
+ metadata = {"model_name": "tirex"}
+
+ response = ForecastResponse.from_arrays_and_metadata(arrays, metadata)
+
+ assert response.point is None
+ assert response.quantiles is None
+ assert response.samples is not None
+ assert np.array_equal(response.samples, samples_data)
+
+ def test_from_arrays_multiple_outputs(self):
+ """Test creating response with multiple output types."""
+ point_data = np.random.rand(32, 24, 1)
+ quantiles_data = np.random.rand(32, 24, 3)
+ arrays = {"point": point_data, "quantiles": quantiles_data}
+ metadata = {"model_name": "flowstate"}
+
+ response = ForecastResponse.from_arrays_and_metadata(arrays, metadata)
+
+ assert response.point is not None
+ assert response.quantiles is not None
+ assert response.samples is None
+
+ def test_from_arrays_validation_requires_output(self):
+ """Test that at least one output array is required."""
+ arrays = {} # No output arrays
+ metadata = {"model_name": "chronos2"}
+
+ with pytest.raises(ValueError, match="Response missing output arrays"):
+ ForecastResponse.from_arrays_and_metadata(arrays, metadata)
+
+ def test_from_arrays_ignores_non_output_arrays(self):
+ """Test that non-output arrays in dict are ignored."""
+ point_data = np.random.rand(32, 24, 1)
+ arrays = {"point": point_data, "x": np.random.rand(32, 100, 1)}
+ metadata = {}
+
+ response = ForecastResponse.from_arrays_and_metadata(arrays, metadata)
+ assert response.point is not None
+ # x is not stored in response
+
+ def test_repr_with_point(self):
+ """Test string representation with point predictions."""
+ response = ForecastResponse(
+ point=np.zeros((32, 24, 1)),
+ metadata={"model_name": "chronos2"},
+ )
+ repr_str = repr(response)
+
+ assert "ForecastResponse" in repr_str
+ assert "point.shape=(32, 24, 1)" in repr_str
+ assert "metadata=" in repr_str
+
+ def test_repr_with_multiple_outputs(self):
+ """Test string representation with multiple outputs."""
+ response = ForecastResponse(
+ point=np.zeros((32, 24, 1)),
+ quantiles=np.zeros((32, 24, 3)),
+ samples=np.zeros((32, 24, 100)),
+ metadata={},
+ )
+ repr_str = repr(response)
+
+ assert "point.shape=(32, 24, 1)" in repr_str
+ assert "quantiles.shape=(32, 24, 3)" in repr_str
+ assert "samples.shape=(32, 24, 100)" in repr_str
+
+ def test_repr_with_no_outputs(self):
+ """Test string representation with no outputs."""
+ response = ForecastResponse(metadata={})
+ repr_str = repr(response)
+
+ assert "ForecastResponse" in repr_str
+ assert "outputs=[None]" in repr_str
+
+ def test_default_factory_metadata(self):
+ """Test that metadata defaults to empty dict."""
+ response = ForecastResponse(point=np.zeros((1, 1, 1)))
+ assert response.metadata == {}
+ assert isinstance(response.metadata, dict)
diff --git a/tests/unit/test_utils.py b/tests/unit/test_utils.py
new file mode 100644
index 0000000..8331115
--- /dev/null
+++ b/tests/unit/test_utils.py
@@ -0,0 +1,861 @@
+"""Unit tests for faim_sdk.utils module.
+
+Tests Arrow serialization and deserialization utilities.
+"""
+
+import numpy as np
+import pytest
+
+from faim_sdk.utils import deserialize_from_arrow, serialize_to_arrow
+
+
+class TestSerializeToArrow:
+ """Tests for serialize_to_arrow function."""
+
+ def test_serialize_single_array(self):
+ """Test serializing a single numpy array."""
+ arrays = {"x": np.array([[1.0, 2.0], [3.0, 4.0]])}
+ result = serialize_to_arrow(arrays)
+
+ assert isinstance(result, bytes)
+ assert len(result) > 0
+
+ def test_serialize_multiple_arrays(self):
+ """Test serializing multiple numpy arrays."""
+ arrays = {
+ "x": np.array([[1.0, 2.0]]),
+ "y": np.array([[3.0, 4.0]]),
+ "z": np.array([[5.0, 6.0]]),
+ }
+ result = serialize_to_arrow(arrays)
+
+ assert isinstance(result, bytes)
+ assert len(result) > 0
+
+ def test_serialize_with_metadata(self):
+ """Test serializing with metadata."""
+ arrays = {"x": np.array([[1.0, 2.0]])}
+ metadata = {"horizon": 10, "model": "chronos2"}
+ result = serialize_to_arrow(arrays, metadata)
+
+ assert isinstance(result, bytes)
+ assert len(result) > 0
+
+ def test_serialize_with_compression_zstd(self):
+ """Test serializing with zstd compression."""
+ arrays = {"x": np.random.rand(100, 100)}
+ result = serialize_to_arrow(arrays, compression="zstd")
+
+ assert isinstance(result, bytes)
+ assert len(result) > 0
+
+ def test_serialize_with_compression_lz4(self):
+ """Test serializing with lz4 compression."""
+ arrays = {"x": np.random.rand(100, 100)}
+ result = serialize_to_arrow(arrays, compression="lz4")
+
+ assert isinstance(result, bytes)
+ assert len(result) > 0
+
+ def test_serialize_without_compression(self):
+ """Test serializing without compression."""
+ arrays = {"x": np.array([[1.0, 2.0]])}
+ result = serialize_to_arrow(arrays, compression=None)
+
+ assert isinstance(result, bytes)
+ assert len(result) > 0
+
+ def test_serialize_different_dtypes(self):
+ """Test serializing arrays with different dtypes."""
+ arrays = {
+ "float32": np.array([[1.0, 2.0]], dtype=np.float32),
+ "float64": np.array([[3.0, 4.0]], dtype=np.float64),
+ "int32": np.array([[5, 6]], dtype=np.int32),
+ "int64": np.array([[7, 8]], dtype=np.int64),
+ }
+ result = serialize_to_arrow(arrays)
+
+ assert isinstance(result, bytes)
+
+ def test_serialize_different_shapes(self):
+ """Test serializing arrays with different shapes."""
+ arrays = {
+ "scalar": np.array([1.0]),
+ "1d": np.array([1.0, 2.0, 3.0]),
+ "2d": np.array([[1.0, 2.0], [3.0, 4.0]]),
+ "3d": np.array([[[1.0, 2.0]], [[3.0, 4.0]]]),
+ }
+ result = serialize_to_arrow(arrays)
+
+ assert isinstance(result, bytes)
+
+ def test_serialize_large_array(self):
+ """Test serializing large arrays."""
+ arrays = {"large": np.random.rand(1000, 100, 10)}
+ result = serialize_to_arrow(arrays)
+
+ assert isinstance(result, bytes)
+ assert len(result) > 0
+
+ def test_serialize_empty_metadata(self):
+ """Test serializing with empty metadata dict."""
+ arrays = {"x": np.array([[1.0]])}
+ result = serialize_to_arrow(arrays, metadata={})
+
+ assert isinstance(result, bytes)
+
+ def test_serialize_none_metadata(self):
+ """Test serializing with None metadata."""
+ arrays = {"x": np.array([[1.0]])}
+ result = serialize_to_arrow(arrays, metadata=None)
+
+ assert isinstance(result, bytes)
+
+ def test_serialize_complex_metadata(self):
+ """Test serializing with complex nested metadata."""
+ arrays = {"x": np.array([[1.0]])}
+ metadata = {
+ "horizon": 10,
+ "quantiles": [0.1, 0.5, 0.9],
+ "config": {"model": "chronos2", "version": "1.0"},
+ }
+ result = serialize_to_arrow(arrays, metadata)
+
+ assert isinstance(result, bytes)
+
+ def test_serialize_skips_none_arrays(self):
+ """Test that None values in arrays dict are skipped."""
+ arrays = {"x": np.array([[1.0]]), "y": None, "z": np.array([[2.0]])}
+ result = serialize_to_arrow(arrays)
+
+ # Should succeed without error
+ assert isinstance(result, bytes)
+
+ def test_serialize_validation_requires_numpy_array(self):
+ """Test that non-numpy arrays raise TypeError."""
+ arrays = {"x": [[1.0, 2.0], [3.0, 4.0]]} # Python list
+
+ with pytest.raises(TypeError, match='Array "x" must be numpy.ndarray'):
+ serialize_to_arrow(arrays)
+
+ def test_serialize_validation_rejects_list(self):
+ """Test that lists raise TypeError."""
+ arrays = {"data": [1, 2, 3, 4, 5]}
+
+ with pytest.raises(TypeError, match="must be numpy.ndarray"):
+ serialize_to_arrow(arrays)
+
+ def test_serialize_non_native_endianness(self):
+ """Test handling of non-native endianness arrays."""
+ # Create array with non-native byte order
+ arr = np.array([[1.0, 2.0]], dtype=">f8") # Big-endian
+ arrays = {"x": arr}
+
+ # Should handle conversion automatically
+ result = serialize_to_arrow(arrays)
+ assert isinstance(result, bytes)
+
+ def test_serialize_non_contiguous_array(self):
+ """Test handling of non-contiguous arrays."""
+ # Create non-contiguous array via transpose
+ arr = np.array([[1.0, 2.0, 3.0], [4.0, 5.0, 6.0]]).T
+ assert not arr.flags.c_contiguous
+
+ arrays = {"x": arr}
+ result = serialize_to_arrow(arrays)
+
+ assert isinstance(result, bytes)
+
+ def test_serialize_deterministic_order(self):
+ """Test that serialization produces deterministic ordering."""
+ arrays1 = {"z": np.array([3.0]), "a": np.array([1.0]), "m": np.array([2.0])}
+ arrays2 = {"a": np.array([1.0]), "m": np.array([2.0]), "z": np.array([3.0])}
+
+ result1 = serialize_to_arrow(arrays1, compression=None)
+ result2 = serialize_to_arrow(arrays2, compression=None)
+
+ # Should produce identical bytes due to sorted keys
+ assert result1 == result2
+
+
+class TestDeserializeFromArrow:
+ """Tests for deserialize_from_arrow function."""
+
+ def test_deserialize_single_array(self):
+ """Test deserializing a single array."""
+ original = {"x": np.array([[1.0, 2.0], [3.0, 4.0]])}
+ serialized = serialize_to_arrow(original)
+
+ arrays, metadata = deserialize_from_arrow(serialized)
+
+ assert "x" in arrays
+ assert np.array_equal(arrays["x"], original["x"])
+
+ def test_deserialize_multiple_arrays(self):
+ """Test deserializing multiple arrays."""
+ original = {
+ "x": np.array([[1.0, 2.0]]),
+ "y": np.array([[3.0, 4.0]]),
+ "z": np.array([[5.0, 6.0]]),
+ }
+ serialized = serialize_to_arrow(original)
+
+ arrays, metadata = deserialize_from_arrow(serialized)
+
+ assert set(arrays.keys()) == {"x", "y", "z"}
+ assert np.array_equal(arrays["x"], original["x"])
+ assert np.array_equal(arrays["y"], original["y"])
+ assert np.array_equal(arrays["z"], original["z"])
+
+ def test_deserialize_with_metadata(self):
+ """Test deserializing with metadata."""
+ original_arrays = {"x": np.array([[1.0]])}
+ original_metadata = {"horizon": 10, "model": "chronos2"}
+ serialized = serialize_to_arrow(original_arrays, original_metadata)
+
+ arrays, metadata = deserialize_from_arrow(serialized)
+
+ assert metadata == original_metadata
+ assert metadata["horizon"] == 10
+ assert metadata["model"] == "chronos2"
+
+ def test_deserialize_preserves_dtype(self):
+ """Test that deserialization preserves dtypes."""
+ original = {
+ "float32": np.array([[1.0, 2.0]], dtype=np.float32),
+ "float64": np.array([[3.0, 4.0]], dtype=np.float64),
+ "int32": np.array([[5, 6]], dtype=np.int32),
+ "int64": np.array([[7, 8]], dtype=np.int64),
+ }
+ serialized = serialize_to_arrow(original)
+
+ arrays, _ = deserialize_from_arrow(serialized)
+
+ assert arrays["float32"].dtype == np.float32
+ assert arrays["float64"].dtype == np.float64
+ assert arrays["int32"].dtype == np.int32
+ assert arrays["int64"].dtype == np.int64
+
+ def test_deserialize_preserves_shape(self):
+ """Test that deserialization preserves shapes."""
+ original = {
+ "scalar": np.array([1.0]),
+ "1d": np.array([1.0, 2.0, 3.0]),
+ "2d": np.array([[1.0, 2.0], [3.0, 4.0]]),
+ "3d": np.array([[[1.0, 2.0]], [[3.0, 4.0]]]),
+ }
+ serialized = serialize_to_arrow(original)
+
+ arrays, _ = deserialize_from_arrow(serialized)
+
+ assert arrays["scalar"].shape == (1,)
+ assert arrays["1d"].shape == (3,)
+ assert arrays["2d"].shape == (2, 2)
+ assert arrays["3d"].shape == (2, 1, 2)
+
+ def test_deserialize_large_array(self):
+ """Test deserializing large arrays."""
+ original = {"large": np.random.rand(1000, 100, 10)}
+ serialized = serialize_to_arrow(original)
+
+ arrays, _ = deserialize_from_arrow(serialized)
+
+ assert "large" in arrays
+ assert arrays["large"].shape == (1000, 100, 10)
+ assert np.array_equal(arrays["large"], original["large"])
+
+ def test_deserialize_empty_metadata(self):
+ """Test deserializing with empty metadata."""
+ original = {"x": np.array([[1.0]])}
+ serialized = serialize_to_arrow(original, metadata={})
+
+ arrays, metadata = deserialize_from_arrow(serialized)
+
+ assert metadata == {}
+
+ def test_deserialize_none_metadata(self):
+ """Test deserializing with None metadata."""
+ original = {"x": np.array([[1.0]])}
+ serialized = serialize_to_arrow(original, metadata=None)
+
+ arrays, metadata = deserialize_from_arrow(serialized)
+
+ assert metadata == {}
+
+ def test_deserialize_complex_metadata(self):
+ """Test deserializing with complex nested metadata."""
+ original_metadata = {
+ "horizon": 10,
+ "quantiles": [0.1, 0.5, 0.9],
+ "config": {"model": "chronos2", "version": "1.0"},
+ }
+ original_arrays = {"x": np.array([[1.0]])}
+ serialized = serialize_to_arrow(original_arrays, original_metadata)
+
+ arrays, metadata = deserialize_from_arrow(serialized)
+
+ assert metadata == original_metadata
+ assert metadata["quantiles"] == [0.1, 0.5, 0.9]
+ assert metadata["config"]["model"] == "chronos2"
+
+
+class TestRoundTrip:
+ """Tests for serialize -> deserialize round-trip consistency."""
+
+ def test_roundtrip_simple_array(self):
+ """Test round-trip for simple array."""
+ original = {"x": np.array([[1.0, 2.0], [3.0, 4.0]])}
+ serialized = serialize_to_arrow(original)
+ arrays, _ = deserialize_from_arrow(serialized)
+
+ assert np.array_equal(arrays["x"], original["x"])
+
+ def test_roundtrip_with_metadata(self):
+ """Test round-trip preserves metadata."""
+ original_arrays = {"x": np.array([[1.0]])}
+ original_metadata = {"horizon": 24, "output_type": "quantiles"}
+
+ serialized = serialize_to_arrow(original_arrays, original_metadata)
+ arrays, metadata = deserialize_from_arrow(serialized)
+
+ assert metadata == original_metadata
+
+ def test_roundtrip_multiple_arrays(self):
+ """Test round-trip for multiple arrays."""
+ original = {
+ "x": np.random.rand(32, 100, 1),
+ "point": np.random.rand(32, 24, 1),
+ "quantiles": np.random.rand(32, 24, 3),
+ }
+ serialized = serialize_to_arrow(original)
+ arrays, _ = deserialize_from_arrow(serialized)
+
+ for key in original:
+ assert key in arrays
+ assert np.array_equal(arrays[key], original[key])
+ assert arrays[key].shape == original[key].shape
+ assert arrays[key].dtype == original[key].dtype
+
+ def test_roundtrip_with_zstd_compression(self):
+ """Test round-trip with zstd compression."""
+ original = {"x": np.random.rand(100, 50)}
+ serialized = serialize_to_arrow(original, compression="zstd")
+ arrays, _ = deserialize_from_arrow(serialized)
+
+ assert np.allclose(arrays["x"], original["x"])
+
+ def test_roundtrip_with_lz4_compression(self):
+ """Test round-trip with lz4 compression."""
+ original = {"x": np.random.rand(100, 50)}
+ serialized = serialize_to_arrow(original, compression="lz4")
+ arrays, _ = deserialize_from_arrow(serialized)
+
+ assert np.allclose(arrays["x"], original["x"])
+
+ def test_roundtrip_different_dtypes(self):
+ """Test round-trip preserves all dtypes."""
+ original = {
+ "float32": np.array([1.5, 2.5], dtype=np.float32),
+ "float64": np.array([3.5, 4.5], dtype=np.float64),
+ "int32": np.array([5, 6], dtype=np.int32),
+ "int64": np.array([7, 8], dtype=np.int64),
+ }
+ serialized = serialize_to_arrow(original, compression=None)
+ arrays, _ = deserialize_from_arrow(serialized)
+
+ for key in original:
+ assert arrays[key].dtype == original[key].dtype
+ assert np.array_equal(arrays[key], original[key])
+
+ def test_roundtrip_different_shapes(self):
+ """Test round-trip preserves all shapes."""
+ original = {
+ "1d": np.array([1.0, 2.0, 3.0]),
+ "2d": np.array([[1.0, 2.0], [3.0, 4.0]]),
+ "3d": np.random.rand(4, 5, 6),
+ "4d": np.random.rand(2, 3, 4, 5),
+ }
+ serialized = serialize_to_arrow(original)
+ arrays, _ = deserialize_from_arrow(serialized)
+
+ for key in original:
+ assert arrays[key].shape == original[key].shape
+ assert np.allclose(arrays[key], original[key])
+
+ def test_roundtrip_exact_values(self):
+ """Test round-trip preserves exact floating point values."""
+ original = {"data": np.array([1.23456789, 9.87654321, -0.123456, 0.0])}
+ serialized = serialize_to_arrow(original, compression=None)
+ arrays, _ = deserialize_from_arrow(serialized)
+
+ # Should be exactly equal (no floating point error)
+ assert np.array_equal(arrays["data"], original["data"])
+
+ def test_roundtrip_negative_values(self):
+ """Test round-trip with negative values."""
+ original = {"data": np.array([[-1.0, -2.0], [-3.0, -4.0]])}
+ serialized = serialize_to_arrow(original)
+ arrays, _ = deserialize_from_arrow(serialized)
+
+ assert np.array_equal(arrays["data"], original["data"])
+
+ def test_roundtrip_zero_values(self):
+ """Test round-trip with zero values."""
+ original = {"data": np.zeros((10, 10))}
+ serialized = serialize_to_arrow(original)
+ arrays, _ = deserialize_from_arrow(serialized)
+
+ assert np.array_equal(arrays["data"], original["data"])
+
+ def test_roundtrip_special_floats(self):
+ """Test round-trip with special float values."""
+ original = {"data": np.array([np.inf, -np.inf, 0.0, -0.0, 1e-300, 1e300])}
+ serialized = serialize_to_arrow(original)
+ arrays, _ = deserialize_from_arrow(serialized)
+
+ assert np.array_equal(arrays["data"], original["data"])
+
+ def test_roundtrip_realistic_forecast_request(self):
+ """Test round-trip for realistic forecast request data."""
+ original_arrays = {
+ "x": np.random.randn(32, 100, 1).astype(np.float32),
+ }
+ original_metadata = {
+ "horizon": 24,
+ "model_version": "1.0",
+ "output_type": "quantiles",
+ "quantiles": [0.1, 0.5, 0.9],
+ }
+
+ serialized = serialize_to_arrow(original_arrays, original_metadata, compression="zstd")
+ arrays, metadata = deserialize_from_arrow(serialized)
+
+ assert np.allclose(arrays["x"], original_arrays["x"])
+ assert arrays["x"].shape == (32, 100, 1)
+ assert arrays["x"].dtype == np.float32
+ assert metadata == original_metadata
+
+ def test_roundtrip_realistic_forecast_response(self):
+ """Test round-trip for realistic forecast response data."""
+ original_arrays = {
+ "point": np.random.randn(32, 24, 1).astype(np.float32),
+ "quantiles": np.random.randn(32, 24, 3).astype(np.float32),
+ }
+ original_metadata = {
+ "model_name": "chronos2",
+ "model_version": "1.0",
+ "inference_time_ms": 123,
+ }
+
+ serialized = serialize_to_arrow(original_arrays, original_metadata, compression="zstd")
+ arrays, metadata = deserialize_from_arrow(serialized)
+
+ assert set(arrays.keys()) == {"point", "quantiles"}
+ assert np.allclose(arrays["point"], original_arrays["point"])
+ assert np.allclose(arrays["quantiles"], original_arrays["quantiles"])
+ assert metadata == original_metadata
+
+
+class TestMultipleInputOutputFormats:
+ """Tests for serialization/deserialization with various input/output format combinations."""
+
+ def test_single_sample_2d_input(self):
+ """Test serialization with single 2D sample (no batch dimension)."""
+ original = {"x": np.array([[1.0, 2.0], [3.0, 4.0]])} # Shape: (seq_len, features)
+ serialized = serialize_to_arrow(original)
+ arrays, _ = deserialize_from_arrow(serialized)
+
+ assert arrays["x"].shape == (2, 2)
+ assert np.array_equal(arrays["x"], original["x"])
+
+ def test_single_sample_3d_input(self):
+ """Test serialization with single 3D sample (batch_size=1)."""
+ original = {"x": np.random.rand(1, 100, 2).astype(np.float32)} # Shape: (1, seq_len, features)
+ serialized = serialize_to_arrow(original)
+ arrays, _ = deserialize_from_arrow(serialized)
+
+ assert arrays["x"].shape == (1, 100, 2)
+ assert np.allclose(arrays["x"], original["x"])
+
+ def test_batch_3d_input(self):
+ """Test serialization with batched 3D input."""
+ original = {"x": np.random.rand(32, 100, 1).astype(np.float32)} # Shape: (batch, seq_len, features)
+ serialized = serialize_to_arrow(original)
+ arrays, _ = deserialize_from_arrow(serialized)
+
+ assert arrays["x"].shape == (32, 100, 1)
+ assert np.allclose(arrays["x"], original["x"])
+
+ def test_multivariate_input(self):
+ """Test serialization with multivariate time series (multiple features)."""
+ original = {"x": np.random.rand(16, 80, 5).astype(np.float32)} # 5 features
+ serialized = serialize_to_arrow(original)
+ arrays, _ = deserialize_from_arrow(serialized)
+
+ assert arrays["x"].shape == (16, 80, 5)
+ assert np.allclose(arrays["x"], original["x"])
+
+ def test_point_output_format(self):
+ """Test serialization of point prediction output."""
+ original = {"point": np.random.rand(32, 24, 1).astype(np.float32)}
+ metadata = {"output_type": "point"}
+ serialized = serialize_to_arrow(original, metadata)
+ arrays, meta = deserialize_from_arrow(serialized)
+
+ assert "point" in arrays
+ assert arrays["point"].shape == (32, 24, 1)
+ assert meta["output_type"] == "point"
+
+ def test_quantiles_output_format(self):
+ """Test serialization of quantile prediction output."""
+ original = {"quantiles": np.random.rand(32, 24, 3).astype(np.float32)}
+ metadata = {"output_type": "quantiles", "quantiles": [0.1, 0.5, 0.9]}
+ serialized = serialize_to_arrow(original, metadata)
+ arrays, meta = deserialize_from_arrow(serialized)
+
+ assert "quantiles" in arrays
+ assert arrays["quantiles"].shape == (32, 24, 3)
+ assert meta["quantiles"] == [0.1, 0.5, 0.9]
+
+ def test_samples_output_format(self):
+ """Test serialization of sample prediction output."""
+ original = {"samples": np.random.rand(32, 24, 100).astype(np.float32)}
+ metadata = {"output_type": "samples", "num_samples": 100}
+ serialized = serialize_to_arrow(original, metadata)
+ arrays, meta = deserialize_from_arrow(serialized)
+
+ assert "samples" in arrays
+ assert arrays["samples"].shape == (32, 24, 100)
+ assert meta["num_samples"] == 100
+
+ def test_multiple_quantile_levels(self):
+ """Test serialization with many quantile levels."""
+ num_quantiles = 9
+ original = {"quantiles": np.random.rand(16, 12, num_quantiles).astype(np.float32)}
+ metadata = {
+ "output_type": "quantiles",
+ "quantiles": [0.05, 0.1, 0.25, 0.5, 0.75, 0.9, 0.95, 0.975, 0.99],
+ }
+ serialized = serialize_to_arrow(original, metadata)
+ arrays, meta = deserialize_from_arrow(serialized)
+
+ assert arrays["quantiles"].shape == (16, 12, 9)
+ assert len(meta["quantiles"]) == 9
+
+ def test_mixed_output_point_and_quantiles(self):
+ """Test serialization with both point and quantile outputs (same total size)."""
+ # Both arrays must have same total elements for Arrow batch
+ original = {
+ "point": np.random.rand(32, 24, 1).astype(np.float32), # 768 elements
+ "quantiles": np.random.rand(32, 8, 3).astype(np.float32), # 768 elements
+ }
+ metadata = {"output_type": "both", "quantiles": [0.1, 0.5, 0.9]}
+ serialized = serialize_to_arrow(original, metadata)
+ arrays, meta = deserialize_from_arrow(serialized)
+
+ assert "point" in arrays
+ assert "quantiles" in arrays
+ assert arrays["point"].shape == (32, 24, 1)
+ assert arrays["quantiles"].shape == (32, 8, 3)
+
+ def test_request_format_input_only(self):
+ """Test serialization of request (input only)."""
+ original = {"x": np.random.rand(32, 100, 1).astype(np.float32)}
+ metadata = {"horizon": 24}
+ serialized = serialize_to_arrow(original, metadata)
+ arrays, meta = deserialize_from_arrow(serialized)
+
+ assert "x" in arrays
+ assert arrays["x"].shape == (32, 100, 1)
+ assert meta["horizon"] == 24
+
+ def test_response_format_output_only(self):
+ """Test serialization of response (output only)."""
+ original = {"point": np.random.rand(32, 24, 1).astype(np.float32)}
+ metadata = {"model_name": "chronos2"}
+ serialized = serialize_to_arrow(original, metadata)
+ arrays, meta = deserialize_from_arrow(serialized)
+
+ assert "point" in arrays
+ assert arrays["point"].shape == (32, 24, 1)
+
+ def test_very_small_batch_input(self):
+ """Test serialization with batch_size=1 (request)."""
+ original = {"x": np.random.rand(1, 50, 1).astype(np.float32)}
+ serialized = serialize_to_arrow(original)
+ arrays, _ = deserialize_from_arrow(serialized)
+
+ assert arrays["x"].shape == (1, 50, 1)
+
+ def test_very_small_batch_output(self):
+ """Test serialization with batch_size=1 (response)."""
+ original = {"point": np.random.rand(1, 10, 1).astype(np.float32)}
+ serialized = serialize_to_arrow(original)
+ arrays, _ = deserialize_from_arrow(serialized)
+
+ assert arrays["point"].shape == (1, 10, 1)
+
+ def test_large_batch_input(self):
+ """Test serialization with large batch size (request)."""
+ batch_size = 256
+ original = {"x": np.random.rand(batch_size, 100, 1).astype(np.float32)}
+ serialized = serialize_to_arrow(original, compression="zstd")
+ arrays, _ = deserialize_from_arrow(serialized)
+
+ assert arrays["x"].shape == (batch_size, 100, 1)
+
+ def test_large_batch_output(self):
+ """Test serialization with large batch size (response)."""
+ batch_size = 256
+ original = {"point": np.random.rand(batch_size, 24, 1).astype(np.float32)}
+ serialized = serialize_to_arrow(original, compression="zstd")
+ arrays, _ = deserialize_from_arrow(serialized)
+
+ assert arrays["point"].shape == (batch_size, 24, 1)
+
+ def test_short_sequence(self):
+ """Test serialization with short sequence length."""
+ original = {"x": np.random.rand(32, 10, 1).astype(np.float32)} # seq_len=10
+ serialized = serialize_to_arrow(original)
+ arrays, _ = deserialize_from_arrow(serialized)
+
+ assert arrays["x"].shape == (32, 10, 1)
+
+ def test_long_sequence(self):
+ """Test serialization with long sequence length."""
+ original = {"x": np.random.rand(8, 1000, 1).astype(np.float32)} # seq_len=1000
+ serialized = serialize_to_arrow(original, compression="zstd")
+ arrays, _ = deserialize_from_arrow(serialized)
+
+ assert arrays["x"].shape == (8, 1000, 1)
+
+ def test_univariate_series_input(self):
+ """Test serialization with univariate time series input (features=1)."""
+ original = {"x": np.random.rand(32, 100, 1).astype(np.float32)}
+ serialized = serialize_to_arrow(original)
+ arrays, _ = deserialize_from_arrow(serialized)
+
+ assert arrays["x"].shape[2] == 1
+
+ def test_univariate_series_output(self):
+ """Test serialization with univariate time series output (features=1)."""
+ original = {"point": np.random.rand(32, 24, 1).astype(np.float32)}
+ serialized = serialize_to_arrow(original)
+ arrays, _ = deserialize_from_arrow(serialized)
+
+ assert arrays["point"].shape[2] == 1
+
+ def test_multivariate_series_input(self):
+ """Test serialization with multivariate time series input (features>1)."""
+ num_features = 10
+ original = {"x": np.random.rand(32, 100, num_features).astype(np.float32)}
+ serialized = serialize_to_arrow(original)
+ arrays, _ = deserialize_from_arrow(serialized)
+
+ assert arrays["x"].shape == (32, 100, num_features)
+
+ def test_multivariate_series_output(self):
+ """Test serialization with multivariate time series output (features>1)."""
+ num_features = 10
+ original = {"point": np.random.rand(32, 24, num_features).astype(np.float32)}
+ serialized = serialize_to_arrow(original)
+ arrays, _ = deserialize_from_arrow(serialized)
+
+ assert arrays["point"].shape == (32, 24, num_features)
+
+ def test_chronos2_request_format(self):
+ """Test serialization matching Chronos2 request format."""
+ original_arrays = {"x": np.random.randn(32, 100, 1).astype(np.float32)}
+ original_metadata = {
+ "horizon": 24,
+ "model_version": "1",
+ "output_type": "quantiles",
+ "quantiles": [0.1, 0.5, 0.9],
+ }
+ serialized = serialize_to_arrow(original_arrays, original_metadata, compression="zstd")
+ arrays, metadata = deserialize_from_arrow(serialized)
+
+ assert arrays["x"].shape == (32, 100, 1)
+ assert metadata["horizon"] == 24
+ assert metadata["output_type"] == "quantiles"
+ assert metadata["quantiles"] == [0.1, 0.5, 0.9]
+
+ def test_chronos2_response_format(self):
+ """Test serialization matching Chronos2 response format with quantiles."""
+ original_arrays = {"quantiles": np.random.randn(32, 24, 3).astype(np.float32)}
+ original_metadata = {
+ "model_name": "chronos2",
+ "model_version": "1.0",
+ "quantiles": [0.1, 0.5, 0.9],
+ "inference_time_ms": 123,
+ }
+ serialized = serialize_to_arrow(original_arrays, original_metadata, compression="zstd")
+ arrays, metadata = deserialize_from_arrow(serialized)
+
+ assert arrays["quantiles"].shape == (32, 24, 3)
+ assert metadata["model_name"] == "chronos2"
+ assert metadata["quantiles"] == [0.1, 0.5, 0.9]
+
+ def test_flowstate_request_format(self):
+ """Test serialization matching FlowState request format."""
+ original_arrays = {"x": np.random.randn(16, 80, 1).astype(np.float32)}
+ original_metadata = {
+ "horizon": 20,
+ "model_version": "1",
+ "output_type": "point",
+ "scale_factor": 100.0,
+ "prediction_type": "mean",
+ }
+ serialized = serialize_to_arrow(original_arrays, original_metadata, compression="lz4")
+ arrays, metadata = deserialize_from_arrow(serialized)
+
+ assert arrays["x"].shape == (16, 80, 1)
+ assert metadata["scale_factor"] == 100.0
+ assert metadata["prediction_type"] == "mean"
+
+ def test_flowstate_response_format(self):
+ """Test serialization matching FlowState response format."""
+ original_arrays = {"point": np.random.randn(16, 20, 1).astype(np.float32)}
+ original_metadata = {
+ "model_name": "flowstate",
+ "model_version": "1.0",
+ "inference_time_ms": 45,
+ }
+ serialized = serialize_to_arrow(original_arrays, original_metadata)
+ arrays, metadata = deserialize_from_arrow(serialized)
+
+ assert arrays["point"].shape == (16, 20, 1)
+ assert metadata["model_name"] == "flowstate"
+
+ def test_tirex_request_format(self):
+ """Test serialization matching TiRex request format."""
+ original_arrays = {"x": np.random.randn(32, 100, 1).astype(np.float32)}
+ original_metadata = {
+ "horizon": 24,
+ "model_version": "1",
+ "output_type": "point",
+ }
+ serialized = serialize_to_arrow(original_arrays, original_metadata)
+ arrays, metadata = deserialize_from_arrow(serialized)
+
+ assert arrays["x"].shape == (32, 100, 1)
+ assert metadata["output_type"] == "point"
+
+ def test_tirex_response_format(self):
+ """Test serialization matching TiRex response format."""
+ original_arrays = {"point": np.random.randn(32, 24, 1).astype(np.float32)}
+ original_metadata = {
+ "model_name": "tirex",
+ "model_version": "1.0",
+ }
+ serialized = serialize_to_arrow(original_arrays, original_metadata)
+ arrays, metadata = deserialize_from_arrow(serialized)
+
+ assert arrays["point"].shape == (32, 24, 1)
+ assert metadata["model_name"] == "tirex"
+
+ def test_different_horizons(self):
+ """Test serialization with different forecast horizons (output only)."""
+ horizons = [1, 5, 10, 24, 48, 100]
+
+ for horizon in horizons:
+ original = {"point": np.random.rand(8, horizon, 1).astype(np.float32)}
+ metadata = {"horizon": horizon}
+ serialized = serialize_to_arrow(original, metadata)
+ arrays, meta = deserialize_from_arrow(serialized)
+
+ assert arrays["point"].shape[1] == horizon
+ assert meta["horizon"] == horizon
+
+ def test_different_compressions_same_data(self):
+ """Test that different compressions produce same deserialized data."""
+ original = {"x": np.random.rand(32, 100, 1).astype(np.float32)}
+
+ # Serialize with different compressions
+ serialized_zstd = serialize_to_arrow(original, compression="zstd")
+ serialized_lz4 = serialize_to_arrow(original, compression="lz4")
+ serialized_none = serialize_to_arrow(original, compression=None)
+
+ # Deserialize all
+ arrays_zstd, _ = deserialize_from_arrow(serialized_zstd)
+ arrays_lz4, _ = deserialize_from_arrow(serialized_lz4)
+ arrays_none, _ = deserialize_from_arrow(serialized_none)
+
+ # All should produce same data
+ assert np.allclose(arrays_zstd["x"], original["x"])
+ assert np.allclose(arrays_lz4["x"], original["x"])
+ assert np.allclose(arrays_none["x"], original["x"])
+ assert np.allclose(arrays_zstd["x"], arrays_lz4["x"])
+ assert np.allclose(arrays_zstd["x"], arrays_none["x"])
+
+ def test_float32_vs_float64(self):
+ """Test serialization with different float precisions."""
+ original_f32 = {"x_f32": np.random.rand(16, 50, 1).astype(np.float32)}
+ original_f64 = {"x_f64": np.random.rand(16, 50, 1).astype(np.float64)}
+
+ # Serialize both
+ serialized_f32 = serialize_to_arrow(original_f32)
+ serialized_f64 = serialize_to_arrow(original_f64)
+
+ # Deserialize and check dtypes preserved
+ arrays_f32, _ = deserialize_from_arrow(serialized_f32)
+ arrays_f64, _ = deserialize_from_arrow(serialized_f64)
+
+ assert arrays_f32["x_f32"].dtype == np.float32
+ assert arrays_f64["x_f64"].dtype == np.float64
+
+ def test_mixed_precision_response(self):
+ """Test serialization with mixed precision arrays in response (same total size)."""
+ # All arrays must have same total elements: 16 * 10 * 3 = 480 elements
+ original = {
+ "point": np.random.rand(16, 10, 3).astype(np.float64), # 480 elements
+ "quantiles": np.random.rand(16, 10, 3).astype(np.float32), # 480 elements
+ }
+ serialized = serialize_to_arrow(original)
+ arrays, _ = deserialize_from_arrow(serialized)
+
+ assert arrays["point"].dtype == np.float64
+ assert arrays["quantiles"].dtype == np.float32
+
+ def test_multiple_output_types_response(self):
+ """Test serialization of multiple output types (same total size)."""
+ # All 800 elements: 32 * 5 * 5 = 800
+ original = {
+ "point": np.random.rand(32, 5, 5).astype(np.float32), # 800 elements
+ "quantiles": np.random.rand(20, 8, 5).astype(np.float32), # 800 elements
+ "samples": np.random.rand(16, 10, 5).astype(np.float32), # 800 elements
+ }
+ serialized = serialize_to_arrow(original, metadata={})
+ arrays, meta = deserialize_from_arrow(serialized)
+
+ assert len(arrays) == 3
+ assert meta == {}
+ for key in original:
+ assert np.allclose(arrays[key], original[key])
+
+ def test_complex_metadata_with_simple_data(self):
+ """Test serialization of simple data with complex metadata."""
+ original = {"x": np.array([[1.0, 2.0]])}
+ metadata = {
+ "horizon": 10,
+ "model_version": "2.1.3",
+ "output_type": "quantiles",
+ "quantiles": [0.01, 0.05, 0.1, 0.25, 0.5, 0.75, 0.9, 0.95, 0.99],
+ "config": {
+ "temperature": 0.8,
+ "top_p": 0.9,
+ "max_tokens": 1000,
+ },
+ "preprocessing": {
+ "normalize": True,
+ "scale_factor": 1.5,
+ "remove_outliers": False,
+ },
+ }
+ serialized = serialize_to_arrow(original, metadata)
+ arrays, meta = deserialize_from_arrow(serialized)
+
+ assert np.array_equal(arrays["x"], original["x"])
+ assert meta == metadata
+ assert meta["config"]["temperature"] == 0.8
+ assert meta["preprocessing"]["normalize"] is True