Skip to main content
Version: Next

Architecture

Synapsys is built in seven layers. Each layer has a single responsibility and depends only on the layers below. This means you can use the mathematical core alone without touching the agent infrastructure, or swap the transport backend without changing any control logic.

Layer diagram

Design decisions

API / math engine separation

The synapsys.api layer is a convenience wrapper only. All mathematics lives in synapsys.core. This allows the API to evolve without touching the numerical core.

LTI class operator overloading

G1 * G2 (series), G1 + G2 (parallel), and G.feedback() allow composing systems with natural block algebra:

T = (C * G).feedback()   # closed loop: C in series with G

TransferFunctionMatrix extends this algebra to MIMO plants: * performs matrix multiplication (series) and + is element-wise (parallel). Simulation and analysis delegate to a minimal StateSpace realisation built lazily by to_state_space().

Broker as mediator

PlantAgent, ControllerAgent, and HardwareAgent do not know how data is sent. They call self._read(channel) and self._write(channel, data), which dispatch to the MessageBroker injected at construction time. The broker routes each channel to the appropriate backend (SharedMemoryBackend, ZMQBrokerBackend, or a custom one) — no control logic changes needed.

This mediator pattern enables many-to-many topologies: a ControllerAgent can simultaneously receive plant output (plant/y) and live parameter updates from a ReconfigAgent (ctrl/config) through the same broker instance.

Transport lifecycle

The transport backend is owned by the caller via the MessageBroker. Agents never call backend.close() directly. This prevents double-free when multiple agents share views of the same memory block. Always call broker.close() in a finally block or at program exit.

Continuous vs discrete

StateSpace(A, B, C, D, dt=0) is continuous. dt > 0 is discrete. The same class supports both:

  • is_stable() uses Re(poles) < 0 for continuous and |poles| < 1 for discrete
  • step() delegates to scipy.signal.step or scipy.signal.dstep automatically
  • evolve(x, u) executes x(k+1) = Ax + Bu step by step for real-time simulation