Skip to main content
Version: 0.2.3

Connecting Your Controller

Any Python callable can be used as a controller in the 3D views. Simply pass your function to the controller= parameter:

CartPoleView(controller=my_function).run()

Expected signature

controller(x: np.ndarray) -> np.ndarray | float
ArgumentTypeDescription
xnp.ndarray shape (n,)full state vector at the current time step
returnnp.ndarray shape (m,) or floatcontrol action (converted to array internally)

The controller is called every dt seconds on the UI thread. For heavy models (> 5 ms inference time), see the Thread-safety section.

tip

When controller=None (default), the lib automatically designs an LQR via sim.linearize() + lqr(Q, R). A custom controller replaces the LQR. Interactive perturbations from the buttons are added after your controller's return value.


Examples

Design the LQR outside the lib and pass the gain K as a controller:

import numpy as np
from synapsys.viz import CartPoleView
from synapsys.simulators import CartPoleSim
from synapsys.algorithms.lqr import lqr

# 1. Get the linearized model
sim = CartPoleSim(m_c=1.0, m_p=0.1, l=0.5)
sim.reset()
ss = sim.linearize(np.zeros(4), np.zeros(1))

# 2. Design LQR with custom weights
Q = np.diag([5.0, 0.1, 500.0, 50.0]) # penalize angle more
R = 0.001 * np.eye(1) # allow larger forces
K, _ = lqr(ss.A, ss.B, Q, R)

# 3. Pass to the view
CartPoleView(
controller=lambda x: np.clip(-K @ x, -100, 100)
).run()

Thread-safety

The controller runs on the main UI thread (same thread as Qt). This means:

  • Fast models (< 2 ms): no problem, use directly.
  • Medium models (2–10 ms): animation will be slightly slow but functional.
  • Slow models (> 10 ms): the UI will freeze. Use a separate thread with a queue:
import threading
import queue
import numpy as np
from synapsys.viz import CartPoleView

# Queue for inter-thread communication
action_queue: queue.Queue[np.ndarray] = queue.Queue(maxsize=1)
state_queue: queue.Queue[np.ndarray] = queue.Queue(maxsize=1)

def slow_model_thread():
while True:
x = state_queue.get()
u = my_slow_model(x) # may take time
try:
action_queue.put_nowait(u)
except queue.Full:
pass

last_u = np.zeros(1)

def controller(x: np.ndarray) -> np.ndarray:
global last_u
try:
state_queue.put_nowait(x)
last_u = action_queue.get_nowait()
except (queue.Full, queue.Empty):
pass
return last_u

threading.Thread(target=slow_model_thread, daemon=True).start()
CartPoleView(controller=controller).run()

Verifying the controller before running

Before passing a controller to the view, you can test it directly with the simulator:

import numpy as np
from synapsys.simulators import CartPoleSim

sim = CartPoleSim()
sim.reset(x0=np.array([0, 0, 0.2, 0]))

x = sim.state
u = my_controller(x)
print("u =", u, "shape =", np.asarray(u).shape) # should be (1,)

y, info = sim.step(np.asarray(u).ravel(), dt=0.02)
print("next state:", info["x"])