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()usesRe(poles) < 0for continuous and|poles| < 1for discretestep()delegates toscipy.signal.steporscipy.signal.dstepautomaticallyevolve(x, u)executesx(k+1) = Ax + Bustep by step for real-time simulation