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¶
Adapters - Protocol implementations
Dependency Injection - Using protocols with DI