Source code for state_space_setup_assistant.schemas

"""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