3.2. Protocols

Protocols define interface contracts that enable loose coupling and dependency injection.

3.2.1. What Are Protocols?

Python protocols (PEP 544) define structural subtyping - any class that implements the required methods satisfies the protocol:

from typing import Protocol


class MyProtocol(Protocol):
    def do_something(self, x: int) -> str: ...


# Any class with do_something(int) -> str satisfies this

3.2.2. NLSQ Protocols

Located in nlsq/interfaces/:

3.2.2.1. OptimizerProtocol

from nlsq.interfaces.optimizer_protocol import OptimizerProtocol


class OptimizerProtocol(Protocol):
    def optimize(
        self, fun: Callable, x0: ArrayLike, args: tuple = (), **kwargs
    ) -> OptimizeResult: ...

3.2.2.2. CurveFitProtocol

from nlsq.interfaces.optimizer_protocol import CurveFitProtocol


class CurveFitProtocol(Protocol):
    def curve_fit(
        self,
        f: Callable,
        xdata: ArrayLike,
        ydata: ArrayLike,
        p0: ArrayLike | None = None,
        sigma: ArrayLike | None = None,
        **kwargs
    ) -> tuple[ArrayLike, ArrayLike]: ...

3.2.2.3. CacheProtocol

from nlsq.interfaces.cache_protocol import CacheProtocol


class CacheProtocol(Protocol):
    def get(self, key: str, default: Any = None) -> Any: ...

    def set(self, key: str, value: Any) -> None: ...

    def clear(self) -> None: ...

3.2.2.4. Orchestration Protocols (v0.6.4)

from nlsq.interfaces.orchestration_protocol import (
    DataPreprocessorProtocol,
    OptimizationSelectorProtocol,
    CovarianceComputerProtocol,
    StreamingCoordinatorProtocol,
)

3.2.3. Implementing Protocols

Simple implementation:

from nlsq.interfaces.optimizer_protocol import CurveFitProtocol


class MyCurveFitter:
    """Custom implementation of CurveFitProtocol."""

    def curve_fit(self, f, xdata, ydata, p0=None, sigma=None, **kwargs):
        # Your custom fitting logic
        from scipy.optimize import curve_fit as scipy_fit

        return scipy_fit(f, xdata, ydata, p0=p0, sigma=sigma, **kwargs)


# Type check
fitter: CurveFitProtocol = MyCurveFitter()  # OK

With type hints:

from typing import Callable
import numpy as np
from numpy.typing import ArrayLike
from nlsq.interfaces.optimizer_protocol import CurveFitProtocol


class TypedFitter:
    def curve_fit(
        self,
        f: Callable,
        xdata: ArrayLike,
        ydata: ArrayLike,
        p0: ArrayLike | None = None,
        sigma: ArrayLike | None = None,
        **kwargs
    ) -> tuple[np.ndarray, np.ndarray]:
        # Implementation
        pass

3.2.4. Protocol Usage

Function accepting protocol:

def run_analysis(
    fitter: CurveFitProtocol, data: list[tuple], model: Callable
) -> list[np.ndarray]:
    """Run analysis with any CurveFitProtocol implementation."""
    results = []
    for x, y in data:
        popt, pcov = fitter.curve_fit(model, x, y)
        results.append(popt)
    return results


# Works with any compatible fitter
from nlsq import CurveFit

results = run_analysis(CurveFit(), data_list, my_model)

# Also works with custom implementation
results = run_analysis(MyCurveFitter(), data_list, my_model)

Testing with mocks:

class MockFitter:
    def curve_fit(self, f, xdata, ydata, **kwargs):
        # Return fixed values for testing
        return np.array([1.0, 2.0]), np.eye(2)


# Use mock in tests
results = run_analysis(MockFitter(), test_data, model)

3.2.5. Runtime Checking

from typing import runtime_checkable, Protocol


@runtime_checkable
class CurveFitProtocol(Protocol):
    def curve_fit(self, f, xdata, ydata, **kwargs): ...


# Now isinstance works
fitter = MyCurveFitter()
assert isinstance(fitter, CurveFitProtocol)  # True

3.2.6. Next Steps