"""Parameter metadata for the controller registry, for building UI forms.
Hand-written schemas for the known designs; unknown (future plugin)
controllers fall back to an inspect.signature-derived schema so they still
get a usable form.
Param kinds understood by the frontend::
square_weight -- scalar / per-diagonal list / full matrix; ``dim`` names
which plant dimension sizes it, ``labels`` which name list
labels the diagonal entries.
tf_weight -- scalar gain or {num: [...], den: [...]} transfer function.
scalar -- plain float.
per_input -- scalar or one value per actuated input.
"""
import importlib.util
import inspect
from typing import Dict, List
def _slycot_available() -> bool:
return (importlib.util.find_spec('control') is not None
and importlib.util.find_spec('slycot') is not None)
SCHEMAS: Dict[str, Dict] = {
'lqr': {
'description': 'Optimal state feedback u = u_eq − Kx minimizing '
'∫ xᵀQx + uᵀRu dt. Needs full state measurement.',
'params': [
{'name': 'Q', 'kind': 'square_weight', 'dim': 'n_states',
'labels': 'state_names', 'default': 1.0,
'help': 'state cost (scalar, per-state diagonal, or matrix)'},
{'name': 'R', 'kind': 'square_weight', 'dim': 'n_inputs',
'labels': 'input_names', 'default': 0.1,
'help': 'input cost'},
],
},
'lqg': {
'description': 'LQR on a Kalman-filter state estimate: dynamic '
'output feedback for noisy/partial measurements.',
'params': [
{'name': 'Q', 'kind': 'square_weight', 'dim': 'n_states',
'labels': 'state_names', 'default': 1.0, 'help': 'state cost'},
{'name': 'R', 'kind': 'square_weight', 'dim': 'n_inputs',
'labels': 'input_names', 'default': 0.1, 'help': 'input cost'},
{'name': 'W', 'kind': 'square_weight', 'dim': 'n_inputs',
'labels': 'input_names', 'default': 1.0,
'help': 'process (actuator) noise covariance'},
{'name': 'V', 'kind': 'square_weight', 'dim': 'n_outputs',
'labels': 'output_names', 'default': 0.01,
'help': 'measurement noise covariance'},
],
},
'hinf_mixsyn': {
'description': 'Mixed-sensitivity H∞ (S/KS/T): shape sensitivity, '
'control effort and rolloff with weights.',
'requires': 'slycot',
'params': [
{'name': 'W1', 'kind': 'tf_weight', 'default': 1.0,
'help': 'sensitivity weight (tracking/disturbance rejection)'},
{'name': 'W2', 'kind': 'tf_weight', 'default': 0.1,
'help': 'control effort weight'},
{'name': 'W3', 'kind': 'tf_weight', 'default': None,
'help': 'complementary sensitivity weight (rolloff)'},
],
},
'hinf': {
'description': 'General H∞ synthesis on a user-supplied augmented '
'plant (advanced; matrices A,B,C,D + n_meas/n_ctrl).',
'requires': 'slycot',
'advanced': True,
'params': [],
},
'pid': {
'description': 'Per-joint PID regulator about the operating point '
'(u = u_eq + PID(δy)); baseline, no model-based '
'guarantees.',
'params': [
{'name': 'Kp', 'kind': 'per_input', 'default': 50.0,
'help': 'proportional gain (scalar or per actuated joint)'},
{'name': 'Ki', 'kind': 'per_input', 'default': 10.0,
'help': 'integral gain'},
{'name': 'Kd', 'kind': 'per_input', 'default': 5.0,
'help': 'derivative gain'},
{'name': 'tau', 'kind': 'scalar', 'default': 0.01,
'help': 'derivative filter time constant [s]'},
],
},
}
def _fallback_schema(name: str) -> Dict:
"""Derive a minimal schema from a registered design's __init__."""
from state_space_control.base import make_controller
try:
from state_space_control import base
from state_space_control import controllers # noqa: F401
cls = base._REGISTRY[name]
except Exception:
return {'description': '', 'params': []}
params = []
for pname, p in inspect.signature(cls.__init__).parameters.items():
if pname == 'self':
continue
default = None if p.default is inspect.Parameter.empty else p.default
params.append({'name': pname, 'kind': 'scalar', 'default': default,
'help': ''})
return {'description': (cls.__doc__ or '').strip().split('\n')[0],
'params': params}
[docs]
def list_controllers() -> List[Dict]:
"""All registered controllers with availability + UI schema."""
from state_space_control.base import available_controllers
slycot = _slycot_available()
out = []
for name in available_controllers():
schema = SCHEMAS.get(name) or _fallback_schema(name)
available = True
hint = None
if schema.get('requires') == 'slycot' and not slycot:
available = False
hint = "pip install control slycot"
out.append({
'name': name,
'available': available,
'install_hint': hint,
'advanced': bool(schema.get('advanced')),
'description': schema.get('description', ''),
'params': schema.get('params', []),
})
return out