"""Exception hierarchy for :mod:`pidtune`. All exceptions raised by this library derive from :class:`PIDTuneError`, so callers can catch a single base class to handle any library-originated failure:: try: gains = tuner.tune(model) except pidtune.PIDTuneError as exc: log.error("tuning failed: %s", exc) Design conventions (see ``docs/design/05_errors_extensibility.md``): * **Never** raise bare :class:`Exception` or :class:`RuntimeError` from library code; always use a class from this module (or a subclass). * Validation of user-supplied parameters raises :class:`ModelValidationError` (for process-model parameters) or :class:`ConfigurationError` (for tuner/simulation options) — both at construction time, so objects are never observable in an invalid state. * Numerical algorithms that fail to converge raise :class:`ConvergenceError` rather than returning ``None`` or NaN-laden results. * Optional third-party dependencies (e.g. SciPy for the optimization-based tuner) are imported lazily; absence raises :class:`MissingDependencyError` with installation instructions in the message. """ from __future__ import annotations __all__ = [ "PIDTuneError", "ModelError", "ModelValidationError", "IdentificationError", "TuningError", "TuningNotApplicableError", "ConvergenceError", "SimulationError", "ConfigurationError", "MissingDependencyError", ] class PIDTuneError(Exception): """Base class for every exception raised by :mod:`pidtune`. Catching this class is guaranteed to intercept any error originating from library code (as opposed to bugs surfacing as e.g. :class:`TypeError` from misuse of the public API). """ class ModelError(PIDTuneError): """Base class for errors related to process-model objects. Raised when a model cannot perform a requested operation — for example, evaluating the step response of a model whose parameters place it outside the supported region, or converting between model representations that are not compatible. """ class ModelValidationError(ModelError, ValueError): """A process model was constructed with invalid parameters. Examples: non-positive time constant, negative dead time, an empty denominator polynomial for a transfer function. Inherits from :class:`ValueError` so generic validation handlers also catch it. The message must name the offending parameter and the constraint it violates, e.g. ``"FOPDT time constant tau must be > 0, got -2.5"``. """ class IdentificationError(PIDTuneError): """Process identification from experimental data failed. Raised by the :mod:`pidtune.identification` routines when input data is unusable (too few samples, no detectable step, non-monotonic time vector) or when a model fit cannot be obtained from the data. """ class TuningError(PIDTuneError): """Base class for errors raised while computing controller gains.""" class TuningNotApplicableError(TuningError): """The selected tuning method cannot be applied to the given model. Examples: applying a Cohen-Coon rule (which requires a dead time) to a model with zero dead time, or requesting a Ziegler-Nichols ultimate-cycle tuning for a model with no ultimate gain. The message must state which method was requested, which model was supplied, and why the combination is invalid — and, where possible, suggest an applicable alternative method. """ class ConvergenceError(TuningError): """An iterative tuning or identification algorithm failed to converge. Raised by relay autotuning (limit cycle never stabilises), the optimization-based tuner (optimizer reports failure or iteration budget exhausted), and iterative model-fitting routines. Carries the number of iterations performed and, where meaningful, the best iterate found so far. """ def __init__( self, message: str, *, iterations: int | None = None, best_result: object | None = None, ) -> None: super().__init__(message) #: Number of iterations completed before giving up, if known. self.iterations = iterations #: Best intermediate result available when convergence failed, #: or ``None``. Type depends on the raising algorithm and is #: documented there. self.best_result = best_result class SimulationError(PIDTuneError): """A closed-loop or open-loop simulation could not be completed. Raised when the integrator produces non-finite states (instability / blow-up), when the requested time grid is invalid, or when plant and controller sampling configurations are inconsistent. """ class ConfigurationError(PIDTuneError, ValueError): """Invalid options were supplied to a tuner, simulator, or controller. Distinct from :class:`ModelValidationError`, which covers *model parameters*; this class covers *algorithm configuration* — e.g. a negative relay amplitude, an unknown objective name for the optimization tuner, or a non-positive controller sample time. """ class MissingDependencyError(PIDTuneError, ImportError): """An optional dependency required for the requested feature is absent. The core library depends only on NumPy. Features that need SciPy (optimization-based tuning, some transfer-function utilities) import it lazily and raise this error if unavailable. The message must include the ``pip install`` command for the relevant extra, e.g. ``pip install pidtune[optim]``. """ def __init__(self, dependency: str, feature: str, extra: str) -> None: message = ( f"The feature '{feature}' requires the optional dependency " f"'{dependency}', which is not installed. " f"Install it with: pip install pidtune[{extra}]" ) super().__init__(message) #: Name of the missing distribution (e.g. ``"scipy"``). self.dependency = dependency #: Human-readable name of the feature that needs it. self.feature = feature #: The pip extra that pulls the dependency in. self.extra = extra