state_space_setup_assistant

The web wizard’s backend. See the guide for the step-by-step tour; the HTTP API is defined in app.create_app.

app — Flask application and HTTP API

Flask app: JSON API + static wizard, everything same-origin.

The linearize and design endpoints deliberately go through the YAML files: they write model.yaml / design.yaml to the session workdir and run the same loaders the CLIs use, so exported artifacts are byte-identical to what produced the numbers on screen.

state_space_setup_assistant.app.to_jsonable(obj)[source]

numpy/complex/NaN-safe JSON conversion (NaN/Inf -> None).

state_space_setup_assistant.app.create_app(session: WizardSession | None = None, prefill_urdf: str | None = None) Flask[source]

session — wizard state

In-memory wizard state.

One WizardSession per server process (single local user). Two browser tabs against the same server share and clobber this state – documented limitation.

class state_space_setup_assistant.session.WizardSession(urdf_source: Optional[str] = None, urdf_path: Optional[str] = None, urdf_xml: Optional[str] = None, robot_name: Optional[str] = None, joints: List[Any] = <factory>, validation: List[Dict] = <factory>, model_cfg: Optional[Dict] = None, model: Any = None, design_cfg: Optional[Dict] = None, result: Any = None, benchmark: Optional[Dict] = None, output_dir: Optional[str] = None, responses: Dict[str, Dict] = <factory>, workdir: str = <factory>)[source]

Bases: object

urdf_source: str | None = None
urdf_path: str | None = None
urdf_xml: str | None = None
robot_name: str | None = None
joints: List[Any]
validation: List[Dict]
model_cfg: Dict | None = None
model: Any = None
design_cfg: Dict | None = None
result: Any = None
benchmark: Dict | None = None
output_dir: str | None = None
responses: Dict[str, Dict]
workdir: str
reset_robot()[source]

Clear everything derived from a previously loaded robot.

reset_model()[source]

Clear everything derived from a previous linearization.

introspect — URDF joint/link parsing

URDF joint/link introspection with the stdlib XML parser.

Pinocchio flattens some of this information away (fixed joints, mimic tags), so the wizard reads the URDF directly for the joint table, slider limits and validation, and leaves the dynamics to urdf_state_space.

class state_space_setup_assistant.introspect.JointInfo(name: str, type: str, parent: str, child: str, axis: Tuple[float, float, float] | None = None, lower: float | None = None, upper: float | None = None, effort: float | None = None, velocity: float | None = None, mimic: str | None = None, movable: bool = False)[source]

Bases: object

name: str
type: str
parent: str
child: str
axis: Tuple[float, float, float] | None = None
lower: float | None = None
upper: float | None = None
effort: float | None = None
velocity: float | None = None
mimic: str | None = None
movable: bool = False
to_dict() Dict[source]
class state_space_setup_assistant.introspect.RobotInfo(name: str, links: List[str], joints: List[state_space_setup_assistant.introspect.JointInfo], materials: Dict = <factory>)[source]

Bases: object

name: str
joints: List[JointInfo]
materials: Dict
property movable_joints: List[JointInfo]
state_space_setup_assistant.introspect.parse_robot(urdf_xml: str) RobotInfo[source]

Parse robot name, links and joints out of a URDF XML string.

state_space_setup_assistant.introspect.parse_joints(urdf_xml: str) List[JointInfo][source]

validation — control-oriented URDF checks

Control-oriented URDF validation, run automatically when a robot loads.

Every check returns a Finding dict {level, code, message, subject} with level ‘ok’ | ‘warn’ | ‘error’. Errors indicate the linearization would be meaningless (bad inertia, disconnected tree); warnings flag things the user should know about (mimic joints, missing limits) but do not block.

state_space_setup_assistant.validation.validate_urdf(urdf_xml: str) list[source]

Run all control-oriented checks; returns a list of Finding dicts.

equilibrium — automatic operating-point finder

Automatic operating-point (equilibrium) finder.

An equilibrium for the actuation set S is a configuration q where the gravity/bias torque on every unactuated joint vanishes:

rnea(q, 0, 0)[unactuated dofs] = 0

Actuated joints can hold any position (u_eq = gravity compensation), so their coordinates are held at the user’s seed values and the root-finding runs only over the unactuated coordinates – a square problem.

Indexing note: the residual and the decision variables are mapped through Pinocchio’s own joint.idx_q / joint.idx_v, never through the position of a name in a Python list; Pinocchio’s q ordering is not guaranteed to match URDF declaration order.

state_space_setup_assistant.equilibrium.find_equilibrium(urdf_xml: str, actuated_joints: List[str], q_seed: Mapping[str, float] | None = None, floating_base: bool = False, tol: float = 1e-09) Dict[source]

Find q_eq near q_seed such that unactuated joints need no torque.

Returns {q_eq: {name: value}, u_eq: {name: value}, residual_norm, converged, iterations, free_joints}.

benchmark — controller comparison

Benchmark mode: synthesize several controllers and compare them fairly.

The whole point of this module is the single shared metric path: simulate_metrics owns the time grid, the integration method and the units for every controller type. It simulates the closed loop once and reads both y(t) and u(t) off that same trajectory – u = -K x(t) for static state feedback, controller-state propagation for dynamic output feedback – so the control-effort column of the comparison table is never computed on different grids or methods per controller type.

state_space_setup_assistant.benchmark.design_controller(plant: Plant, name: str, params: Dict, timeout: float = 30.0)[source]

Run make_controller(name, **params).design(plant) in a killable subprocess.

Parameter validation (make_controller/__init__) runs in-process so bad params raise ValueError immediately; only .design() — which may call into Fortran code that cannot be interrupted — runs in the child.

state_space_setup_assistant.benchmark.simulate_metrics(result, input_index: int = 0, t: ndarray | None = None, n_points: int = 600) Dict[source]

Step-response metrics for one designed controller.

A unit step enters at the plant input (disturbance rejection task, the natural experiment for regulators about an equilibrium); y(t) and u(t) are read off one simulation.

state_space_setup_assistant.benchmark.robustness_metrics(result) Dict[source]

Peak sensitivity Ms (any size) + gain/phase margins (SISO loops).

state_space_setup_assistant.benchmark.run_benchmark(plant: Plant, entries: List[Dict], n_points: int = 600, timeout: float = 30.0) Dict[source]

Design + evaluate each entry {controller, params} on a shared grid.

Failed syntheses become rows with an ‘error’ field, never exceptions.

yamlgen — config file generation

Builders for the two YAML artifacts, in the existing schemas.

The wizard never calls the math engines with ad-hoc arguments: it writes these dicts to disk and runs urdf_state_space.build_from_yaml / the same controller path as ss_design on them, so the exported files are exactly what produced the on-screen results.

state_space_setup_assistant.yamlgen.model_yaml_dict(urdf_source: str, joints: Dict[str, Dict], velocities: bool = False, output_joints: List[str] | None = None, floating_base: bool = False, export_npz: str | None = None, export_mat: str | None = None, dt: float | None = None) Dict[source]

Build a model.yaml dict (schema of urdf_state_space.config).

joints maps joint name -> {actuated?: bool, damping?: float, q_eq?: float}; empty per-joint entries are dropped.

state_space_setup_assistant.yamlgen.design_yaml_dict(controller: str, params: Dict | None = None) Dict[source]

Build a design.yaml dict (schema of state_space_control’s ss_design).

state_space_setup_assistant.yamlgen.dump_yaml(cfg: Dict, path: str) str[source]

schemas — controller parameter metadata

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.
state_space_setup_assistant.schemas.list_controllers() List[Dict][source]

All registered controllers with availability + UI schema.

export — artifact bundle writer

Write the artifact bundle for a finished wizard run.

Everything in the bundle reproduces through the existing CLIs:

ros2 run urdf_state_space urdf2ss model.yaml ros2 run state_space_control ss_design plant.npz design.yaml -o controller.npz

state_space_setup_assistant.export.write_bundle(session, output_dir: str, include_mat: bool = False, dt: float | None = None) Dict[source]

Write all artifacts; returns {files: […], commands: […]}.

ros2_control_export — runtime controller YAML writer

Writes the <name>_ros2_control.yaml file consumed by kontrolem_controllers at runtime — see the export contract.

Build the C++-consumable <name>_ros2_control.yaml artifact.

controller.npz (written by ControllerResult.save_npz) carries the plant/controller matrices but not joint_names/q_eq – those live only on the linearized StateSpaceModel. A ros2_control controller needs both (to turn a raw measured joint position into the deviation state the gain/ compensator was designed for), so this module builds one flat dict from the model and the result together, meant to be dumped with yamlgen.dump_yaml right after result.save_npz in export.py.

Schema (flat nested lists only, so a C++ yaml-cpp reader needs no NumPy support):

schema_version: 1 controller_type: lqr # or lqg / hinf / hinf_mixsyn / pid name: lqr joint_names: […] # full state order: x = [q - q_eq, qdot] q_eq: […] actuated_joint_names: […] # order matches K’s rows / u u_eq: […] output_names: […] # used only by dynamic-compensator designs K: [[…], …] # present iff a static-gain design ctrl_A/ctrl_B/ctrl_C/ctrl_D: [[…], …] # present instead of K otherwise info: {…} # informational scalars only, not consumed

state_space_setup_assistant.ros2_control_export.ros2_control_yaml_dict(model, result) Dict[source]

Build the *_ros2_control.yaml dict from a StateSpaceModel (session.model) and a ControllerResult (session.result).

Exactly one of result.K / result.controller must be set, matching ControllerResult’s own invariant.

resources — package:// resolution

Resource resolution: package:// URIs and containment-guarded mesh paths.

state_space_setup_assistant.resources.load_urdf(source: str, base_dir: str = '.') tuple[source]

Resolve source (path or package:// URI) and return (absolute_path, processed_urdf_xml). Runs xacro on .xacro files.

state_space_setup_assistant.resources.safe_share_path(package: str, relative: str) str | None[source]

Absolute path of relative inside package’s share directory, or None if it escapes the share dir (traversal) or does not exist.

Raises LookupError if the package itself is unknown (workspace not built/sourced).

cli — the ss_setup_assistant entry point

Entry point: run the setup assistant web server and open the browser.

ros2 run state_space_setup_assistant ss_setup_assistant

–urdf package://rws_description/urdf/rws.urdf

Serves on 127.0.0.1 by default (single local user; see README: one browser tab at a time).

state_space_setup_assistant.cli.main(argv=None) int[source]