"""IMC (lambda) and SIMC tuning rules. References ---------- * D. E. Rivera, M. Morari and S. Skogestad, "Internal Model Control. 4. PID Controller Design", *Ind. Eng. Chem. Process Des. Dev.*, 25, 252–265, 1986. * S. Skogestad, "Simple analytic rules for model reduction and PID controller tuning", *Journal of Process Control*, 13, 291–309, 2003. Survey context: ``docs/design/03_tuning_methods.md`` §5. """ from __future__ import annotations from typing import Any, ClassVar from pidtune.models.base import ProcessModel from pidtune.models.fopdt import FOPDT from pidtune.models.sopdt import SOPDT from pidtune.tuners.base import ControllerType, Tuner, TuningResult, register_tuner __all__ = ["IMCTuner", "SIMCTuner"] @register_tuner class IMCTuner(Tuner): """IMC / lambda tuning with a user-chosen closed-loop time constant. The single tuning knob is ``λ`` (``lambda_``), the desired closed-loop time constant: small λ ⇒ aggressive, large λ ⇒ robust/slow. Using a first-order Padé approximation of the dead time, the FOPDT PID rules (Rivera et al. 1986) are:: Kp = (τ + θ/2) / (K · (λ + θ/2)) Ti = τ + θ/2 Td = τ·θ / (2τ + θ) and the PI rules (dead time lumped into the filter):: Kp = τ / (K · (λ + θ)) Ti = τ For :class:`~pidtune.models.sopdt.SOPDT` models ``K·exp(-θs) / ((τ₁s+1)(τ₂s+1))`` the PID rules are:: Kp = (τ₁ + τ₂) / (K · (λ + θ)) Ti = τ₁ + τ₂ Td = τ₁·τ₂ / (τ₁ + τ₂) Choosing lambda --------------- If ``lambda_`` is not given, the default ``λ = max(lambda_factor·τ, θ)`` is used with ``lambda_factor = 1.0`` — a moderate choice; pass a smaller λ for tighter control. ``λ ≤ 0`` raises :class:`~pidtune.exceptions.TuningError`. Unlike ZN/Cohen-Coon, IMC handles ``θ == 0`` gracefully. ``TuningResult.metadata`` keys: ``{"lambda": float}``. """ name: ClassVar[str] = "imc" supported_controller_types: ClassVar[frozenset] = frozenset( {ControllerType.PI, ControllerType.PID} ) def __init__(self, *, lambda_factor: float = 1.0) -> None: """Configure the default-λ heuristic. Parameters ---------- lambda_factor: Multiplier on the dominant time constant used when no explicit ``lambda_`` is passed to :meth:`tune`. Must be positive. """ self.lambda_factor = float(lambda_factor) def supports(self, model: ProcessModel) -> bool: """Accept FOPDT and SOPDT models.""" return isinstance(model, (FOPDT, SOPDT)) def tune( self, model: ProcessModel, *, controller_type: ControllerType = ControllerType.PID, lambda_: float | None = None, **options: Any, ) -> TuningResult: """Apply IMC rules to a FOPDT or SOPDT model. Parameters ---------- model: :class:`~pidtune.models.fopdt.FOPDT` or :class:`~pidtune.models.sopdt.SOPDT`. controller_type: PI or PID (PI is FOPDT-only; SOPDT requires PID). lambda_: Desired closed-loop time constant. ``None`` (default) selects ``max(lambda_factor·τ_dominant, θ)``. **options: No further options are accepted. """ raise NotImplementedError("Implemented in Milestone 3.") @register_tuner class SIMCTuner(Tuner): """Skogestad's SIMC ("Simple/Skogestad IMC") rules. SIMC improves on plain IMC for disturbance rejection on lag-dominant processes by capping the integral time. For FOPDT with tuning constant ``τc`` (default ``τc = θ``, the tight-but-robust recommendation):: Kp = τ / (K · (τc + θ)) Ti = min(τ, 4·(τc + θ)) PI is the primary SIMC structure for FOPDT. For SOPDT, a series-form PID is prescribed (``Td = τ₂``) and converted to the library's ideal form before being returned:: Kp = τ₁ / (K · (τc + θ)), Ti = min(τ₁, 4·(τc + θ)), Td = τ₂ ``θ == 0`` is allowed only with explicit ``tau_c``; otherwise the default ``τc = θ = 0`` would be degenerate and :class:`~pidtune.exceptions.TuningError` is raised. ``TuningResult.metadata`` keys: ``{"tau_c": float, "ti_capped": bool, "source_form": "series"}`` (the latter only for SOPDT/PID results). """ name: ClassVar[str] = "simc" supported_controller_types: ClassVar[frozenset] = frozenset( {ControllerType.PI, ControllerType.PID} ) def supports(self, model: ProcessModel) -> bool: """Accept FOPDT and SOPDT models.""" return isinstance(model, (FOPDT, SOPDT)) def tune( self, model: ProcessModel, *, controller_type: ControllerType = ControllerType.PI, tau_c: float | None = None, **options: Any, ) -> TuningResult: """Apply SIMC rules. Parameters ---------- model: FOPDT (PI or PID) or SOPDT (PID only). controller_type: Defaults to PI, the canonical SIMC structure for FOPDT. tau_c: Closed-loop tuning constant. ``None`` selects ``τc = θ``. Must be positive when given. **options: No further options are accepted. """ raise NotImplementedError("Implemented in Milestone 3.")