Dynamics & process control¶
The differentiable ODE integrators, dynamic holdups and dynamic flowsheets, the classical PID controller, and the tuning/optimal-control helpers for the time domain.
See the dynamics & process control guide for worked examples.
Dynamics¶
dynamics
¶
Differentiable dynamic (time-domain) process simulation for Fugacio.
The fugacio.sim blocks elsewhere are steady-state; this subpackage adds the
time dimension while keeping everything end-to-end differentiable:
integrate: the ODE integration core: a fixed output-gridodeint(explicit and stiff implicit steppers, differentiable in both modes) and an adaptiveintegratewith a continuous-adjointcustom_vjp;units: dynamic unit operations carried as ODEs in conserved holdups (buffer/level tanks, mixing tanks, dynamic CSTR, dynamic flash, lumped thermal mass / heat exchanger, gas receiver), reusing the steady-state thermodynamics for the instantaneous constitutive relations;flowsheet:DynamicFlowsheet, a declarative assembly of dynamic units and controllers into one global ODE that is simulated over time and differentiated through;optimize: dynamic optimization and estimation (optimal control over input trajectories, time-series parameter estimation), composing the existing optimizers with the integrator.
Modules:
| Name | Description |
|---|---|
flowsheet |
Assemble dynamic units and control loops into one differentiable flowsheet ODE. |
integrate |
Differentiable time integration of ordinary differential equations. |
optimize |
Dynamic optimization, estimation, and controller tuning. |
units |
Dynamic unit operations: holdups carried as ODE states. |
Classes:
| Name | Description |
|---|---|
DynamicFlowsheet |
A declarative dynamic flowsheet: feeds, dynamic units, and control loops. |
DynamicResult |
Trajectory returned by |
ODEResult |
Outcome of an adaptive |
DynamicEstimateResult |
Outcome of |
OptimalControlResult |
Outcome of |
DynamicCSTR |
A non-isothermal, constant-volume liquid CSTR: the canonical control plant. |
DynamicFlash |
An isothermal-isobaric flash drum with liquid holdup and equilibrium vapor. |
DynamicUnit |
Abstract base class for a dynamic unit operation. |
GasReceiver |
An isothermal gas surge drum: pressure builds and decays with the inventory. |
LevelTank |
An isothermal, well-mixed liquid surge tank with level and composition dynamics. |
MixingTank |
A constant-holdup, perfectly mixed blending tank (composition first-order lag). |
ThermalMass |
A heated/cooled, well-mixed stirred tank: lumped temperature dynamics. |
Functions:
| Name | Description |
|---|---|
simulate |
Integrate a bare |
odeint |
Integrate |
odeint_final |
Integrate from |
estimate_dynamics |
Fit parameters |
optimal_control |
Find the input trajectory minimizing a running-plus-terminal cost. |
tune_pid |
Tune controller gains by minimizing a closed-loop error integral. |
DynamicFlowsheet
dataclass
¶
DynamicFlowsheet(
feeds: dict[str, FeedSpec] = dict(),
units: dict[str, _Unit] = dict(),
loops: list[_Loop] = list(),
manips: list[_Manip] = list(),
_order: list[str] = list(),
)
A declarative dynamic flowsheet: feeds, dynamic units, and control loops.
Example::
fs = DynamicFlowsheet()
fs.feed("feed", feed_stream) # constant or (t, theta) -> Stream
fs.add(tank, inputs=["feed"]) # a LevelTank named "tank"
fs.control(pid, measurement=("tank", "level"), # level controller ...
setpoint=2.0, actuator=("tank", "flow"))# ... manipulating outlet flow
result = fs.simulate(ts=jnp.linspace(0, 100, 201))
level = result.measurement("tank", "level")
Units are advanced in registration order; reference an upstream outlet as
"unit.port" (port index) and a feed by its name.
Methods:
| Name | Description |
|---|---|
feed |
Register a feed stream (a fixed |
add |
Register a dynamic |
control |
Close a feedback loop: |
manipulate |
Drive a manipulated variable open-loop from a schedule |
initial_state |
Build the global initial state (unit holdups + controller states). |
rhs |
Global state derivative |
simulate |
Integrate the flowsheet over the output grid |
feed
¶
feed(name: str, spec: FeedSpec) -> DynamicFlowsheet
Register a feed stream (a fixed Stream or (t, theta) -> Stream).
add
¶
add(
unit: DynamicUnit, *, inputs: Sequence[str] = ()
) -> DynamicFlowsheet
Register a dynamic unit with its inlet sources (feeds or "unit.port").
control
¶
control(
pid: PID,
*,
measurement: MeasurementSpec,
setpoint: SetpointSpec,
actuator: tuple[str, str],
u0: ArrayLike | None = None,
) -> DynamicFlowsheet
Close a feedback loop: pid drives actuator to hold measurement.
manipulate
¶
manipulate(
unit: str, mv: str, fn: Callable[[Array, Any], Array]
) -> DynamicFlowsheet
Drive a manipulated variable open-loop from a schedule fn(t, theta).
initial_state
¶
Build the global initial state (unit holdups + controller states).
rhs
¶
Global state derivative d(state)/dt (the function handed to the integrator).
simulate
¶
simulate(
ts: Array,
*,
y0: dict[str, Any] | None = None,
theta: Any = None,
method: str = "rk4",
substeps: int = 4,
) -> DynamicResult
Integrate the flowsheet over the output grid ts and return a DynamicResult.
Parameters:
| Name | Type | Description | Default |
|---|---|---|---|
ts
|
Array
|
Strictly increasing output times. |
required |
y0
|
dict[str, Any] | None
|
Optional global initial state (defaults to each unit's initial state and a bumpless controller start). |
None
|
theta
|
Any
|
Differentiable parameter pytree threaded to feeds, setpoints, manipulations and units. |
None
|
method
|
str
|
Integration method (see |
'rk4'
|
substeps
|
int
|
Inner steps per output interval. |
4
|
The returned trajectories of measurements and controller outputs are
recomputed from the state trajectory, so they are differentiable in
theta and y0 as well.
DynamicResult
¶
Bases: NamedTuple
Trajectory returned by DynamicFlowsheet.simulate.
Attributes:
| Name | Type | Description |
|---|---|---|
ts |
Array
|
Output times (shape |
states |
dict[str, Any]
|
Per-unit state trajectory (a dict |
measurements |
dict[str, dict[str, Array]]
|
Per-unit measurement trajectory (a dict |
controls |
dict[str, Array]
|
Manipulated-variable trajectory (a dict |
Methods:
| Name | Description |
|---|---|
measurement |
Trajectory of a named measurement for a unit. |
ODEResult
¶
Bases: NamedTuple
Outcome of an adaptive integrate call.
Attributes:
| Name | Type | Description |
|---|---|---|
y |
Any
|
Final state pytree at |
n_steps |
Array
|
Total attempted steps (accepted + rejected). |
n_accepted |
Array
|
Accepted steps. |
success |
Array
|
Whether the march reached |
DynamicEstimateResult
¶
Bases: NamedTuple
Outcome of estimate_dynamics.
Attributes:
| Name | Type | Description |
|---|---|---|
theta |
Any
|
Estimated parameter pytree. |
trajectory |
Any
|
Model trajectory at the estimate (leading time axis). |
cost |
Array
|
Half-sum-of-squares residual at the estimate. |
result |
OptimizeResult
|
The underlying |
OptimalControlResult
¶
Bases: NamedTuple
Outcome of optimal_control.
Attributes:
| Name | Type | Description |
|---|---|---|
u |
Any
|
Optimal piecewise-constant control (one value, or vector, per interval). |
cost |
Array
|
Optimal total cost (running integral plus terminal). |
trajectory |
Any
|
State trajectory under the optimal control (leading time axis). |
result |
OptimizeResult
|
The underlying |
DynamicCSTR
dataclass
¶
DynamicCSTR(
name: str,
components: tuple[str, ...],
reactions: Reaction | Sequence[Reaction],
rate_laws: Any,
volume: ArrayLike = 1.0,
q: ArrayLike = 1.0,
rho_cp: ArrayLike = 4000000.0,
ua: ArrayLike = 0.0,
jacket_t: ArrayLike = 298.15,
p: ArrayLike = 101325.0,
c0: Array | None = None,
t0: ArrayLike = 298.15,
)
Bases: DynamicUnit
A non-isothermal, constant-volume liquid CSTR: the canonical control plant.
Works in concentration space: state is the vector of species concentrations
C (mol/m^3) and the reactor temperature T. The reactor has volume
volume and a constant volumetric throughput q, so the residence time is
volume / q. The energy balance carries the heat of reaction (from
fugacio.thermo.delta_h_rxn) and a jacket term UA (T_jacket - T) with
a constant volumetric heat capacity rho_cp (J/m^3/K). Exothermic operation
reproduces the classic ignition/extinction and limit-cycle behaviour, which is
why this is the reactor-control benchmark.
Manipulated variables: "jacket_t" (K) and "q" (m^3/s). Measurements:
temperature, concentration, heat_release.
DynamicFlash
dataclass
¶
DynamicFlash(
name: str,
components: tuple[str, ...],
t: ArrayLike = 298.15,
p: ArrayLike = 101325.0,
eos: CubicEOS = PR,
kij: Array | None = None,
vapor_draw: ArrayLike = 0.0,
liquid_draw: ArrayLike = 0.0,
m0: Array | None = None,
)
Bases: DynamicUnit
An isothermal-isobaric flash drum with liquid holdup and equilibrium vapor.
State is the per-component liquid holdup M (mol). The vapour drawn off is in
instantaneous phase equilibrium with the well-mixed holdup (its composition is
the equilibrium vapour of a flash_pt on the holdup at (T, P)) and
leaves at a commanded rate; the liquid product leaves at the holdup composition.
This captures the composition response of a separator to feed and draw
disturbances while reusing the rigorous EOS equilibrium.
Manipulated variables: "vapor_draw" (mol/s), "liquid_draw" (mol/s).
Measurements: holdup, x (liquid composition), y (vapour composition).
DynamicUnit
¶
Bases: ABC
Abstract base class for a dynamic unit operation.
Methods:
| Name | Description |
|---|---|
initial_state |
Return the initial state pytree for this unit. |
evaluate |
Return the state derivative, outlet streams and measurements at |
measured |
State-only measurements (level, T, P, composition) available to controllers. |
initial_state
abstractmethod
¶
initial_state() -> Any
Return the initial state pytree for this unit.
evaluate
abstractmethod
¶
evaluate(
state: Any,
inlets: tuple[Stream, ...],
controls: Controls | None = None,
theta: Any = None,
) -> UnitStep
Return the state derivative, outlet streams and measurements at state.
measured
abstractmethod
¶
State-only measurements (level, T, P, composition) available to controllers.
These depend on the unit state alone, not on the inlet streams, so a
controller can read them before the units are advanced, which keeps the
instantaneous control algebra explicit (see DynamicFlowsheet).
GasReceiver
dataclass
¶
GasReceiver(
name: str,
components: tuple[str, ...],
volume: ArrayLike = 1.0,
t: ArrayLike = 298.15,
outlet: str = "valve",
valve_cv: ArrayLike = 0.001,
p_down: ArrayLike = 101325.0,
flow_setpoint: ArrayLike = 0.0,
n0: Array | None = None,
)
Bases: DynamicUnit
An isothermal gas surge drum: pressure builds and decays with the inventory.
State is the component mole holdup N of gas in a fixed volume; the
pressure follows the ideal-gas law P = (sum N) R T / V. Gas leaves either
at a commanded flow (outlet="flow", manipulated variable "flow") or
through a valve to a downstream pressure p_down (outlet="valve",
manipulated variable "opening"), giving the textbook pressure-control plant.
Manipulated variables: "flow" (mol/s) or "opening" (-). Measurements:
pressure, holdup, outlet_flow.
LevelTank
dataclass
¶
LevelTank(
name: str,
components: tuple[str, ...],
area: ArrayLike = 1.0,
t: ArrayLike = 298.15,
p: ArrayLike = 101325.0,
outlet: str = "pump",
valve_cv: ArrayLike = 1.0,
flow_setpoint: ArrayLike = 0.0,
n0: Array | None = None,
)
Bases: DynamicUnit
An isothermal, well-mixed liquid surge tank with level and composition dynamics.
State is the per-component liquid holdup N (mol). The liquid leaves either
at a commanded molar flow (outlet="pump", manipulated variable "flow")
or through a valve whose discharge follows a Torricelli law
(outlet="valve", manipulated variable "opening" in [0, 1]); the
outlet always carries the well-mixed holdup composition. The liquid level is
inferred from the holdup volume and the tank cross-sectional area using the
real liquid density.
Manipulated variables: "flow" (mol/s, pump mode) or "opening" (-, valve
mode). Measurements: level, volume, holdup, outlet_flow.
MixingTank
dataclass
¶
MixingTank(
name: str,
components: tuple[str, ...],
holdup: ArrayLike = 1.0,
t: ArrayLike = 298.15,
p: ArrayLike = 101325.0,
x0: Array | None = None,
)
Bases: DynamicUnit
A constant-holdup, perfectly mixed blending tank (composition first-order lag).
With a fixed molar holdup holdup and equal in/out flow, the outlet
composition relaxes toward the (instantaneous) inlet composition with a time
constant equal to the residence time holdup / inlet_flow. State is the
mole-fraction vector x.
Measurements: residence_time, inlet_flow, x0 ... (per-component
fractions are also returned under frac).
ThermalMass
dataclass
¶
ThermalMass(
name: str,
components: tuple[str, ...],
holdup: ArrayLike = 100.0,
ua: ArrayLike = 0.0,
t_ambient: ArrayLike = 298.15,
p: ArrayLike = 101325.0,
duty_setpoint: ArrayLike = 0.0,
t0: ArrayLike = 298.15,
)
Bases: DynamicUnit
A heated/cooled, well-mixed stirred tank: lumped temperature dynamics.
A fixed molar holdup of liquid is heated by a duty Q (manipulated
variable "duty", W), exchanges sensible heat with the inlet flow, and loses
heat to ambient through ua (W/K). State is the bulk temperature T;
the outlet carries the inlet flows at T. Heat capacities come from the
ideal-gas correlations of fugacio.thermo.
Manipulated variable: "duty" (W). Measurements: temperature, duty.
simulate
¶
simulate(
rhs: Callable[[Array, Any, Any], Any],
y0: Any,
ts: Array,
theta: Any = None,
*,
method: str = "rk4",
substeps: int = 4,
) -> Any
Integrate a bare rhs(t, y, theta) over ts (a thin odeint alias).
For ad-hoc dynamic models that are not expressed as a DynamicFlowsheet.
Returns the state trajectory pytree (leading time axis), differentiable in
y0 and theta.
odeint
¶
odeint(
func: RHS,
y0: Any,
ts: Array,
theta: Any = None,
*,
method: str = "rk4",
substeps: int = 1,
) -> Any
Integrate dy/dt = func(t, y, theta) over the output grid ts.
The state is advanced from ts[0] to ts[-1], taking substeps uniform
inner steps of the chosen method between successive output points, and the
state is recorded at every entry of ts. Because the step pattern is static
the whole integration is an ordinary jax.lax.scan, hence differentiable
in forward and reverse mode with respect to y0 and theta, no custom
rule needed.
Parameters:
| Name | Type | Description | Default |
|---|---|---|---|
func
|
RHS
|
Right-hand side |
required |
y0
|
Any
|
Initial state pytree at |
required |
ts
|
Array
|
1-D array of strictly increasing output times (length >= 2). |
required |
theta
|
Any
|
Optional differentiable parameter pytree forwarded to |
None
|
method
|
str
|
One of |
'rk4'
|
substeps
|
int
|
Number of inner integration steps per output interval (>= 1);
raise it to cut discretisation error without densifying |
1
|
Returns:
| Type | Description |
|---|---|
Any
|
The trajectory as a pytree of the same structure as |
Any
|
time axis of length |
Any
|
|
odeint_final
¶
odeint_final(
func: RHS,
y0: Any,
t0: ArrayLike,
t1: ArrayLike,
theta: Any = None,
*,
method: str = "rk4",
steps: int = 100,
) -> Any
Integrate from t0 to t1 and return only the final state.
A convenience wrapper over odeint for the common case of a single
interval with steps uniform steps; differentiable in y0 and theta.
estimate_dynamics
¶
estimate_dynamics(
dynamics: Callable[[Array, Any, Any], Any],
y0: Any,
ts: Array,
data: Array,
theta0: Any,
*,
observe: Callable[[Any], Array] | None = None,
weights: Array | None = None,
integrator: str = "rk4",
substeps: int = 2,
max_iter: int = 100,
) -> DynamicEstimateResult
Fit parameters theta so the simulated trajectory matches data.
Minimizes sum (w (observe(traj) - data))^2 by Levenberg-Marquardt, with the
trajectory produced by integrating dynamics(t, y, theta) from y0 over
ts. observe maps the trajectory pytree to the measured quantity
(defaults to the trajectory itself); weights optionally scales residuals.
Returns:
| Type | Description |
|---|---|
DynamicEstimateResult
|
A |
optimal_control
¶
optimal_control(
dynamics: Callable[[Array, Any, Any, Any], Any],
y0: Any,
ts: Array,
u_init: Array,
stage_cost: Callable[[Array, Any, Any, Any], Array],
*,
terminal_cost: Callable[[Any, Any], Array]
| None = None,
theta: Any = None,
bounds: tuple[Any, Any] | None = None,
method: str = "bfgs",
integrator: str = "rk4",
substeps: int = 2,
max_iter: int = 100,
) -> OptimalControlResult
Find the input trajectory minimizing a running-plus-terminal cost.
The control is piecewise constant on the ts grid: u_init has one entry
(scalar or vector) per interval. The augmented state (y, J) integrates the
running cost J' = stage_cost(t, y, u, theta) alongside the dynamics
y' = dynamics(t, y, u, theta), and the total J(t_f) + terminal_cost(y_f)
is minimized over the control with gradients straight through the simulation.
Parameters:
| Name | Type | Description | Default |
|---|---|---|---|
dynamics
|
Callable[[Array, Any, Any, Any], Any]
|
|
required |
y0
|
Any
|
Initial state pytree. |
required |
ts
|
Array
|
Output/decision grid (length |
required |
u_init
|
Array
|
Initial control guess, shape |
required |
stage_cost
|
Callable[[Array, Any, Any, Any], Array]
|
Running cost |
required |
terminal_cost
|
Callable[[Any, Any], Array] | None
|
Optional terminal cost |
None
|
theta
|
Any
|
Optional fixed parameter pytree. |
None
|
bounds
|
tuple[Any, Any] | None
|
Optional |
None
|
method
|
str
|
Unconstrained optimizer used over the control (e.g. |
'bfgs'
|
integrator
|
str
|
ODE integration scheme for the augmented state (e.g. |
'rk4'
|
substeps
|
int
|
Integration substeps taken between consecutive |
2
|
max_iter
|
int
|
Maximum number of optimizer iterations. |
100
|
Returns:
| Type | Description |
|---|---|
OptimalControlResult
|
An |
tune_pid
¶
tune_pid(
response: Callable[[Any], Array],
gains0: Any,
setpoint: ArrayLike,
ts: Array,
*,
objective: str = "iae",
bounds: tuple[Any, Any] | None = None,
method: str = "bfgs",
max_iter: int = 100,
) -> OptimizeResult
Tune controller gains by minimizing a closed-loop error integral.
response(gains) must build the closed loop from the gains pytree, simulate
it, and return the controlled-variable trajectory sampled on ts. This
function then minimizes the chosen error integral ("iae", "ise" or
"itae") against setpoint over the gains: gradients flow through the
whole simulated loop, so the tune is exact first-order, not a grid search.
Parameters:
| Name | Type | Description | Default |
|---|---|---|---|
response
|
Callable[[Any], Array]
|
|
required |
gains0
|
Any
|
Initial gains pytree (e.g. a dict |
required |
setpoint
|
ArrayLike
|
Target value for the controlled variable. |
required |
ts
|
Array
|
Time grid matching |
required |
objective
|
str
|
Error integral to minimize. |
'iae'
|
bounds
|
tuple[Any, Any] | None
|
Optional |
None
|
method
|
str
|
Unconstrained optimizer used over the gains (e.g. |
'bfgs'
|
max_iter
|
int
|
Maximum number of optimizer iterations. |
100
|
Returns:
| Type | Description |
|---|---|
OptimizeResult
|
The |
Control¶
control
¶
Classical and differentiable process control for Fugacio.
This subpackage supplies the control side of dynamic simulation:
pid: a realizable, anti-windupPIDcontroller whose gains are a differentiable pytree (so they can be tuned by gradient descent), carried as ODE states inside a dynamic flowsheet;blocks: linear blocks (first/second order, FOPDT, lead-lag) with both analytic step responses and state-space realizations, plus static actuator nonlinearities;metrics: step-response performance metrics (overshoot, rise/settling time, IAE/ISE/ITAE) for reporting and as tuning objectives;tuning: FOPDT model identification and the classical tuning rules (Ziegler-Nichols, Cohen-Coon, IMC/lambda, AMIGO);linearize: exact autodiff linearization of a nonlinear plant into aStateSpace, with poles, Bode response, and controllability/observability.
Modules:
| Name | Description |
|---|---|
blocks |
Linear dynamic blocks and analytic step responses for control studies. |
linearize |
Linearize a nonlinear dynamic model about an operating point, by autodiff. |
metrics |
Time-domain performance metrics for a response trajectory. |
pid |
A differentiable PID controller in realizable, anti-windup form. |
tuning |
PID tuning rules and first-order-plus-dead-time model identification. |
Classes:
| Name | Description |
|---|---|
StateSpace |
A linear time-invariant state-space model |
StepInfo |
A bundle of step-response metrics. |
PID |
A filtered, anti-windup PID controller (a differentiable pytree). |
PIDState |
Internal controller state carried through the dynamic simulation. |
FOPDTModel |
A first-order-plus-dead-time process model |
Functions:
| Name | Description |
|---|---|
dead_band |
Symmetric dead band of total width |
first_order_ss |
State-space |
first_order_step |
Response of |
fopdt_step |
Response of a first-order-plus-dead-time process to a step of size |
lead_lag |
Step response of a lead-lag compensator |
rate_limit |
Limit a desired rate of change to |
saturate |
Clamp |
second_order_ss |
State-space |
second_order_step |
Step response of |
bode |
Bode magnitude (dB) and phase (degrees) of a SISO system over |
controllability |
Controllability matrix |
dc_gain |
Steady-state gain |
frequency_response |
Complex frequency response |
is_controllable |
Whether the controllability matrix has full state rank. |
is_observable |
Whether the observability matrix has full state rank. |
is_stable |
Whether every pole has strictly negative real part (asymptotic stability). |
observability |
Observability matrix |
poles |
Eigenvalues of |
iae |
Integral of the absolute error, |
ise |
Integral of the squared error, |
itae |
Integral of time-weighted absolute error, |
overshoot |
Fractional overshoot of the peak beyond the setpoint (0 if none). |
peak_time |
Time of the response extremum in the step direction. |
rise_time |
Time to rise from |
settling_time |
Last time the response leaves the |
steady_state_error |
Offset of the final value from the setpoint, |
step_info |
Compute the standard step-response metrics for a trajectory |
p_only |
Convenience constructor for a proportional-only controller. |
pi |
Convenience constructor for a PI controller (no derivative action). |
amigo |
AMIGO (Astrom-Hagglund) robust tuning from an FOPDT model. |
cohen_coon |
Cohen-Coon tuning from an FOPDT model (better than ZN for larger |
fit_fopdt |
Identify an FOPDT model from a step response |
imc_tuning |
Internal-model-control (lambda) tuning from an FOPDT model. |
ziegler_nichols |
Ziegler-Nichols open-loop (reaction-curve) tuning from an FOPDT model. |
StateSpace
¶
Bases: NamedTuple
A linear time-invariant state-space model x' = A x + B u, y = C x + D u.
Attributes:
| Name | Type | Description |
|---|---|---|
a |
Array
|
State matrix |
b |
Array
|
Input matrix |
c |
Array
|
Output matrix |
d |
Array
|
Feedthrough matrix |
StepInfo
¶
Bases: NamedTuple
A bundle of step-response metrics.
Attributes:
| Name | Type | Description |
|---|---|---|
overshoot |
Array
|
Fractional overshoot beyond the setpoint. |
peak_time |
Array
|
Time of the response extremum. |
rise_time |
Array
|
10-90% rise time. |
settling_time |
Array
|
2% settling time. |
steady_state_error |
Array
|
Final offset |
iae |
Array
|
Integral of absolute error. |
PID
dataclass
¶
PID(
kc: ArrayLike,
tau_i: ArrayLike = inf,
tau_d: ArrayLike = 0.0,
beta: ArrayLike = 1.0,
gamma: ArrayLike = 0.0,
u_bias: ArrayLike = 0.0,
u_min: ArrayLike = -inf,
u_max: ArrayLike = inf,
tau_t: ArrayLike = 0.0,
n_filter: float = 10.0,
direction: str = "reverse",
)
A filtered, anti-windup PID controller (a differentiable pytree).
Attributes:
| Name | Type | Description |
|---|---|---|
kc |
ArrayLike
|
Proportional gain (output units per measurement unit). |
tau_i |
ArrayLike
|
Integral (reset) time, s. Use |
tau_d |
ArrayLike
|
Derivative (rate) time, s. |
beta |
ArrayLike
|
Setpoint weight on the proportional term ( |
gamma |
ArrayLike
|
Setpoint weight on the derivative term (usually |
u_bias |
ArrayLike
|
Output bias / nominal output at zero error (manual reset). |
u_min |
ArrayLike
|
Lower output limit (saturation). |
u_max |
ArrayLike
|
Upper output limit (saturation). |
tau_t |
ArrayLike
|
Anti-windup tracking time, s. |
n_filter |
float
|
Derivative-filter ratio (filter time is |
direction |
str
|
|
Methods:
| Name | Description |
|---|---|
init_state |
Initial controller state for bumpless start at output |
output |
Saturated controller output for the current state, setpoint and measurement. |
derivative |
Time derivative of the controller state (for embedding in a flowsheet ODE). |
step |
Discrete one-step update by explicit Euler; returns |
init_state
¶
init_state(
pv0: ArrayLike, u0: ArrayLike | None = None
) -> PIDState
Initial controller state for bumpless start at output u0 (default bias).
The integral term is preloaded so the controller's initial output equals
u0 (or u_bias if u0 is None) when the measurement is at
pv0 and the setpoint equals pv0; the derivative filter starts at the
measurement so the initial derivative action is zero.
output
¶
output(
state: PIDState, setpoint: ArrayLike, pv: ArrayLike
) -> Array
Saturated controller output for the current state, setpoint and measurement.
derivative
¶
Time derivative of the controller state (for embedding in a flowsheet ODE).
Returns (di/dt, dx_d/dt) as a PIDState. The integral derivative
includes the back-calculation anti-windup term so it stops winding up while
the output is saturated.
step
¶
step(
state: PIDState,
setpoint: ArrayLike,
pv: ArrayLike,
dt: ArrayLike,
) -> tuple[Array, PIDState]
Discrete one-step update by explicit Euler; returns (output, new_state).
For standalone digital-controller loops. Inside a continuous dynamic
simulation prefer derivative so the controller integrates with the
same solver as the plant.
PIDState
¶
Bases: NamedTuple
Internal controller state carried through the dynamic simulation.
Attributes:
| Name | Type | Description |
|---|---|---|
i |
Array
|
Integral term contribution to the output (already scaled by the integral gain), in output units. |
x_d |
Array
|
First-order-filtered measurement used to form the derivative term. |
FOPDTModel
¶
Bases: NamedTuple
A first-order-plus-dead-time process model K e^{-L s} / (tau s + 1).
Attributes:
| Name | Type | Description |
|---|---|---|
gain |
Array
|
Steady-state gain |
tau |
Array
|
Time constant |
dead_time |
Array
|
Dead time / transport delay |
dead_band
¶
Symmetric dead band of total width 2*width centred at zero.
first_order_ss
¶
first_order_ss(
gain: ArrayLike, tau: ArrayLike
) -> tuple[Array, Array, Array, Array]
State-space (A, B, C, D) of K/(tau s + 1) (one state).
first_order_step
¶
Response of K/(tau s + 1) to a step of size u at t = 0.
y(t) = K u (1 - exp(-t/tau)) for t >= 0.
fopdt_step
¶
fopdt_step(
t: ArrayLike,
gain: ArrayLike,
tau: ArrayLike,
dead_time: ArrayLike,
*,
u: ArrayLike = 1.0,
) -> Array
Response of a first-order-plus-dead-time process to a step of size u.
y(t) = K u (1 - exp(-(t - L)/tau)) for t >= L (the dead time L),
else 0. The workhorse model for PID tuning.
lead_lag
¶
lead_lag(
t: ArrayLike,
gain: ArrayLike,
tau_lead: ArrayLike,
tau_lag: ArrayLike,
*,
u: ArrayLike = 1.0,
) -> Array
Step response of a lead-lag compensator K (tau_lead s + 1)/(tau_lag s + 1).
rate_limit
¶
Limit a desired rate of change to +/- max_rate.
saturate
¶
Clamp u to [u_min, u_max].
second_order_ss
¶
second_order_ss(
gain: ArrayLike, wn: ArrayLike, zeta: ArrayLike
) -> tuple[Array, Array, Array, Array]
State-space (A, B, C, D) of the standard second-order block (two states).
second_order_step
¶
second_order_step(
t: ArrayLike,
gain: ArrayLike,
wn: ArrayLike,
zeta: ArrayLike,
*,
u: ArrayLike = 1.0,
) -> Array
Step response of K wn^2 / (s^2 + 2 zeta wn s + wn^2) (size u).
Handles the under-, critically-, and over-damped regimes with a single smooth
expression (the under/over branches are selected by jax.numpy.where, so
the result is differentiable in zeta through zeta = 1 as well).
bode
¶
bode(ss: StateSpace, omega: Array) -> tuple[Array, Array]
Bode magnitude (dB) and phase (degrees) of a SISO system over omega.
controllability
¶
controllability(ss: StateSpace) -> Array
Controllability matrix [B, A B, ..., A^{n-1} B].
dc_gain
¶
dc_gain(ss: StateSpace) -> Array
Steady-state gain -C A^{-1} B + D (the response to a unit step at t -> inf).
frequency_response
¶
frequency_response(ss: StateSpace, omega: Array) -> Array
Complex frequency response H(j omega) = C (j omega I - A)^{-1} B + D.
Returns an array of shape (len(omega), p, m) of complex transfer-function
values.
is_controllable
¶
is_controllable(ss: StateSpace) -> Array
Whether the controllability matrix has full state rank.
is_observable
¶
is_observable(ss: StateSpace) -> Array
Whether the observability matrix has full state rank.
is_stable
¶
is_stable(ss: StateSpace) -> Array
Whether every pole has strictly negative real part (asymptotic stability).
observability
¶
observability(ss: StateSpace) -> Array
Observability matrix [C; C A; ...; C A^{n-1}].
iae
¶
Integral of the absolute error, integral |sp - y| dt.
ise
¶
Integral of the squared error, integral (sp - y)^2 dt.
itae
¶
Integral of time-weighted absolute error, integral t |sp - y| dt.
overshoot
¶
Fractional overshoot of the peak beyond the setpoint (0 if none).
(y_peak - sp) / (sp - y0) for a positive step; y0 defaults to the first
sample. Returns 0 when the response does not exceed the setpoint.
peak_time
¶
Time of the response extremum in the step direction.
rise_time
¶
Time to rise from lo to hi fraction of the step (nan if never reached).
settling_time
¶
settling_time(
t: Array,
y: Array,
setpoint: ArrayLike,
*,
tol: float = 0.02,
) -> Array
Last time the response leaves the +/- tol band around the setpoint.
Returns the time after which |y - sp| <= tol * |sp - y0| holds for the rest
of the record (t[-1] if it never settles within the record).
steady_state_error
¶
Offset of the final value from the setpoint, sp - y[-1].
step_info
¶
Compute the standard step-response metrics for a trajectory (t, y).
p_only
¶
p_only(kc: ArrayLike, **kwargs: ArrayLike) -> PID
Convenience constructor for a proportional-only controller.
pi
¶
pi(
kc: ArrayLike, tau_i: ArrayLike, **kwargs: ArrayLike
) -> PID
Convenience constructor for a PI controller (no derivative action).
amigo
¶
amigo(
model: FOPDTModel,
*,
controller: str = "PI",
**kwargs: ArrayLike,
) -> PID
AMIGO (Astrom-Hagglund) robust tuning from an FOPDT model.
cohen_coon
¶
cohen_coon(
model: FOPDTModel,
*,
controller: str = "PID",
**kwargs: ArrayLike,
) -> PID
Cohen-Coon tuning from an FOPDT model (better than ZN for larger L/tau).
fit_fopdt
¶
fit_fopdt(
t: Array,
y: Array,
u_step: ArrayLike = 1.0,
*,
guess: FOPDTModel | None = None,
) -> FOPDTModel
Identify an FOPDT model from a step response (t, y) to an input step u_step.
Fits (K, tau, L) by Levenberg-Marquardt least squares against the analytic
FOPDT step response. A data-driven initial guess is used when guess is
omitted (gain from the final value; tau and L from the 28.3% / 63.2%
response times). The fit is differentiable in the data.
imc_tuning
¶
imc_tuning(
model: FOPDTModel,
*,
tau_c: ArrayLike | None = None,
controller: str = "PI",
**kwargs: ArrayLike,
) -> PID
Internal-model-control (lambda) tuning from an FOPDT model.
tau_c is the desired closed-loop time constant; it defaults to
max(0.1 tau, 0.8 L), a robust middle-of-the-road choice. Larger tau_c
gives a slower but more robust loop.
ziegler_nichols
¶
ziegler_nichols(
model: FOPDTModel,
*,
controller: str = "PID",
**kwargs: ArrayLike,
) -> PID
Ziegler-Nichols open-loop (reaction-curve) tuning from an FOPDT model.
controller is "P", "PI" or "PID". Aggressive (quarter-amplitude
decay) tuning, a classic baseline, often detuned in practice.