Direct Sampled-Data Control Systems Toolbox — Python port of MATLAB DirectSD v3.0
Project description
DirectSD — Python Package
github.com/RooDK0x7b4/DirectSD-Python
Python port of the DirectSD MATLAB Toolbox v3.0 by Konstantin Polyakov (1995–2006).
DirectSD is a toolbox for analysis and design of sampled-data (hybrid) control systems using:
- Polynomial (Diophantine-equation) methods
- Lifting / FR-operator approach (exact inter-sample modelling)
- State-space (LQR / Kalman / Riccati ARE) methods
- H2 and H∞ optimal control
- Global optimization (Strongin information algorithm, SA simplex, Hilbert-curve search)
- L1 / L∞ convex synthesis via Youla parameterisation (requires
cvxpy)
Installation
pip install numpy scipy # runtime dependencies
pip install -e . # development install from source
Or with optional extras:
pip install -e ".[dev]" # adds pytest + matplotlib
pip install -e ".[convex]" # adds cvxpy for L1/L∞ convex synthesis
pip install -e ".[control]" # adds python-control interoperability
pip install -e ".[all]" # installs all optional dependencies
Package Structure
directsd/
├── __init__.py # flat public API — import everything from here
│
├── polynomial/ # Poln class, operations, spectral factorisation, transforms
│ ├── poln.py # Poln — the @poln OOP class equivalent
│ ├── operations.py # compat, deg, gcd, coprime, triple, factor,
│ │ # striplz, recip, vec, dioph, dioph2
│ ├── spectral.py # sfactor, sfactfft
│ ├── transforms.py # dtfm (ZOH discrete Laplace), ztrm (modified Z)
│ └── utils.py # bilintr, improper, diophsys, tf2nd,
│ # separtf, sumzpk, bilinss
│
├── zpk/ # Zpk class (root-list zero-pole-gain representation)
│ └── zpk.py
│
├── linalg/ # Linear algebra utilities
│ ├── matrices.py # toep, hank, givens, house, lyap, dlyap
│ └── linsys.py # linsys (QR / SVD with iterative refinement)
│
├── analysis/ # Sampled-data system analysis
│ ├── norms.py # sdh2norm, sdhinorm, dinfnorm, dahinorm,
│ │ # h2norm_ct, hinfnorm_ct
│ │ # ── all via Lyapunov / Hamiltonian ARE,
│ │ # no frequency gridding
│ ├── charpol.py # charpol, sdmargin
│ └── errors.py # quaderr, sdl2err, sd2doferr
│
├── design/ # Controller design
│ ├── lifting.py # lift_h2, lift_l2, compute_gamma
│ │ # ── Van Loan exact inter-sample lifting
│ │ # (FR-operator / Chen-Francis method)
│ ├── polynomial.py # ch2, sdh2, sdl2, dhinf, sd2dof, polquad
│ └── convex.py # sdl1_reg, sd_mixed_h2_l1, sd_constrained,
│ # sdl1norm, youla_basis [requires cvxpy]
│
├── sspace/ # State-space design
│ ├── plant.py # GeneralizedPlant class (augmented plant representation)
│ ├── design.py # h2reg, hinfreg, sdh2reg, sdhinfreg,
│ │ # sdfast, separss
│ └── hinfsd.py # Native discrete Hinf synthesis: sdhimod (all 5
│ # discretization formulas) + hinfone/hinfone1
│ # (both gamma=1 synthesis methods) -- sdhinfreg's
│ # primary path, exact rather than the bilinear
│ # DT->CT->DT approximation
│
├── tf/ # Block-diagram arithmetic
│ ├── interconnect.py # mul, neg, add, feedback, nd, to_lti
│ └── __init__.py # re-exports all interconnect functions
│
├── glopt/ # Global optimization
│ ├── optimize.py # neldermead, simanneal, randsearch,
│ │ # crandsearch, dual_annealing, sector, banana
│ └── advanced.py # Full dsdglopt port: updateopt, uniproj, u2range,
│ # randbeta, randgamma, sa_testfun,
│ # val2bin, bin2val, coord2hilb, hilb2coord,
│ # r2range, r1range, admproj, par2cp, cp2par,
│ # guesspoles, k2ksi, go_par2k,
│ # f_sdh2p, f_sdl2p, go_sdh2p, go_sdl2p,
│ # sasimplex, arandsearch,
│ # infglob, infglobc, optglob, optglobc
│
├── examples/ # demos.py (25 MATLAB demo ports), help_examples.py
│ # (26 dsd_help.chm worked examples), examples.py
│ # (11 core-utility examples), _common.py (shared helpers)
│
└── tests/
└── test_directsd.py # 254 pytest tests (235 core + 19 convex,
# convex skipped automatically without cvxpy)
Quick Start
Polynomial arithmetic
from directsd import Poln, s_var, gcd, sfactor, dioph
s = s_var()
p = Poln([1, 3, 2], 's') # s² + 3s + 2
q = Poln([1, 1], 's') # s + 1
print(p * q) # s³ + 4s² + 5s + 2
print(p.roots) # [-2. -1.]
print(gcd(p, q)) # s + 1
# Spectral factorization: R(s) = N(s)·N(-s) → find N
R = Poln([-1, 0, 1], 's') # 1 − s² (Hermitian)
fs, _ = sfactor(R)
print(fs) # s + 1
# Diophantine equation: X·A + Y·B = C
x, y, err, cond = dioph(Poln([1,1],'s'), Poln([1,0],'s'), Poln([1],'s'))
Lifting → H2 design (recommended workflow)
import scipy.signal as sig
from directsd import lift_h2, h2reg, compute_gamma
import numpy as np
# Continuous-time generalized plant (standard 2×2 form)
plant = sig.StateSpace(
np.array([[-1., 0.5], [0., -2.]]),
np.array([[1., 0.], [0., 1.]]),
np.array([[1., 0.], [0., 1.]]),
np.array([[0., 1.], [1., 0.]])
)
T = 0.1 # sampling period
# Step 1 — lift to exact discrete equivalent (Van Loan / FR-operator)
P_lifted, gamma, _ = lift_h2(plant, T)
print(f"Lifted plant: {P_lifted.A.shape}, γ = {gamma:.4f}")
# Step 2 — H2-optimal controller on lifted plant
K, h2n = h2reg(P_lifted, n_meas=1, n_ctrl=1)
# Step 3 — exact total H2 cost
total_cost = np.sqrt(h2n**2 + gamma)
print(f"Optimal H2 cost: {total_cost:.4f}")
Norms (algebraic, no frequency gridding)
from directsd import sdh2norm, sdhinorm, h2norm_ct, hinfnorm_ct, dinfnorm
import scipy.signal as sig
plant = sig.lti([1], [1, 1])
# Continuous-time norms
print(h2norm_ct(plant)) # 0.7071 (= 1/√2)
gamma, w = hinfnorm_ct(plant) # 1.0 at ω=0
print(gamma, w)
# Sampled-data norms
K = ([0.5], [1.0])
print(sdh2norm(plant, K, T=0.1))
gamma, w = sdhinorm(plant, K, T=0.1)
print(gamma, w)
Stability analysis
from directsd import charpol, sdmargin
import scipy.signal as sig
plant = sig.lti([1], [1, 1])
K = ([1.0], [1.0])
T = 0.1
delta = charpol(plant, K, T) # characteristic polynomial coefficients
margin, poles = sdmargin(plant, K, T)
print(f"Stability margin: {margin:.4f}") # positive = stable
print(f"Closed-loop poles: {poles}")
Global optimization
from directsd import neldermead, dual_annealing, simanneal, sector, banana
# Nelder-Mead (deterministic)
x, f, n = neldermead(banana, [-1.2, 1.0], tol=1e-8)
print(f"Banana min: x={x}, f={f:.2e}")
# Dual annealing (scipy hybrid, faster and more robust than simanneal)
x, f, res = dual_annealing(banana, [(-3,3),(-3,3)], seed=42)
print(f"Dual SA min: x={x}, f={f:.2e}, evals={res.nfev}")
# Stability sector
alpha, beta = sector([-1+2j, -1-2j, -3+0j])
print(f"Degree of stability α={alpha}, oscillation β={beta}")
Advanced global optimization (dsdglopt full port)
from directsd import (
infglob, optglob, sasimplex, arandsearch,
par2cp, cp2par, admproj, guesspoles,
coord2hilb, hilb2coord, sa_testfun, randbeta, randgamma,
)
# Strongin information algorithm — 1D Lipschitz minimisation
f = lambda x: (x - 0.3)**2 + 0.1*np.sin(20*x)
x_best, z_best, n_iter, trace = infglob(f, {'maxIter': 300, 'r': 2.5})
# Multi-run with zooming
x_best, z_best, coef, loops, trace = optglob(f, {'maxLoop': 10, 'maxIter': 100})
# SA + Nelder-Mead hybrid
x, y, nevals = sasimplex(sa_testfun, [0.5, 0.5], {'startTemp': 5.0, 'maxFunEvals': 5000})
# Hilbert space-filling curve (N-D ↔ 1D)
coord = np.array([0.3, 0.7, 0.5])
x1d = coord2hilb(coord, precision=8) # 3D → scalar in [0,1]
back = hilb2coord(x1d, dimensions=3, precision=8)
# Stability-sector polynomial parameterisation
Delta, DeltaZ, poles = par2cp([0.4, 0.6, 0.5], alpha=0.2, beta=np.inf)
rho = cp2par(DeltaZ, alpha=0.2, beta=np.inf) # inverse
Convex synthesis — L1 and mixed H2/L1 (requires pip install cvxpy)
The Youla (Q) parameterisation turns all stabilising controllers into an affine family, making L1 and mixed-norm problems convex.
import numpy as np
import scipy.signal as sig
from directsd import lift_h2, sdl1_reg, sd_mixed_h2_l1, sd_constrained, sdl1norm
# Step 1 — continuous-time generalised plant (standard 2×2 form)
ct_plant = sig.StateSpace(
np.array([[-1.]]),
np.array([[1., 1.]]),
np.array([[1.], [1.]]),
np.array([[0., 1.], [1., 0.]])
)
T = 0.1
# Step 2 — lift to exact discrete equivalent
P_lifted, _, _ = lift_h2(ct_plant, T)
# L1-optimal controller (minimises peak output amplitude)
K_l1, l1_norm, Q_fir, info = sdl1_reg(P_lifted, N_fir=20)
print(f"L1-norm (peak-to-peak gain): {l1_norm:.4f} [{info['status']}]")
# Mixed H2/L1: minimise H2 subject to L1 ≤ bound
K_mix, cost, _, _ = sd_mixed_h2_l1(P_lifted, N_fir=20, l1_bound=5.0)
print(f"H2 cost: {cost['h2']:.4f}, L1: {cost['l1']:.4f}")
# Hard envelope constraint
lo = -1.5 * np.ones((15, 1))
hi = 1.5 * np.ones((15, 1))
K_con, achieved, _, _ = sd_constrained(
P_lifted, N_fir=20, objective='h2', envelope=(lo, hi)
)
print(f"Constrained H2: {achieved['h2']:.4f}, linf: {achieved['linf']:.4f}")
# L1-norm analysis of any existing closed loop (no cvxpy needed)
l1, h = sdl1norm(sig.lti([1], [1, 2, 1]), ([0.5], [1.0]), T=0.1)
print(f"Closed-loop L1-norm: {l1:.4f}")
MATLAB → Python mapping
| MATLAB | Python |
|---|---|
poln(a,'s') |
Poln(a, 's') |
s, z, p, q, d |
s_var(), z_var(), … |
gcd(a,b) |
gcd(a, b) |
coprime(a,b) |
coprime(a, b) |
factor(p,'s') |
factor(p, 's') |
sfactor(p) |
sfactor(p) |
sfactfft(p) |
sfactfft(p) |
dioph(a,b,c) |
dioph(a, b, c) |
dtfm(G,T,0) |
dtfm(G, T) |
ztrm(G,T,mu) |
ztrm(G, T, mu) |
bilintr(F,'tustin',T) |
bilintr(F, 'tustin', T) |
improper(R) |
improper(R) |
toep(a,r,c) |
toep(a, r, c) |
linsys(A,B,'svd') |
linsys(A, B, method='svd') |
sdgh2mod(sys,T) |
lift_h2(plant_ss, T) |
sdh2simple(sys,T) |
lift_l2(plant_ss, T) |
charpol(sys,K) |
charpol(plant, K, T) |
sdmargin(sys,K) |
sdmargin(plant, K, T) |
sdh2norm(sys,K) |
sdh2norm(plant, K, T) |
sdhinorm(sys,K) |
sdhinorm(plant, K, T) |
dinfnorm(sys) |
dinfnorm(sys) |
quaderr(A,B,E,M) |
quaderr(plant_cl, T) |
sdl2err(sys,K) |
sdl2err(plant, K, T) |
ch2(sys) |
ch2(plant) |
sdh2(sys,T) |
sdh2(plant, T) |
sdl2(sys,T) |
sdl2(plant, T) |
dhinf(plant_d) |
dhinf(plant_d) |
sd2dof(sys,K,T) |
sd2dof(plant, K_fb, T) |
h2reg(sys,1,1) |
h2reg(sys, n_meas=1, n_ctrl=1) |
hinfreg(sys,1,1) |
hinfreg(sys, n_meas=1, n_ctrl=1) |
sdh2reg(sys,T) |
sdh2reg(plant, T) |
sdhinfreg(sys,T) |
sdhinfreg(plant, T) |
sdfast(sys,T) |
sdfast(plant, T) |
neldermead(f,x0) |
neldermead(f, x0) |
simanneal(f,x0,opt) |
simanneal(f, x0, options=opt) |
sector(poles) |
sector(poles) |
sasimplex(f,x0,opt) |
sasimplex(f, x0, options=opt) |
arandsearch(f,x,opt) |
arandsearch(f, x0, options=opt) |
infglob(f,opt) |
infglob(f, options=opt) |
infglobc(f,opt) |
infglobc(f, options=opt) |
optglob(f,opt) |
optglob(f, options=opt) |
optglobc(f,opt) |
optglobc(f, options=opt) |
par2cp(rho,alpha,beta) |
par2cp(rho, alpha, beta) |
cp2par(DeltaZ,alpha,beta) |
cp2par(DeltaZ, alpha, beta) |
admproj(p,alpha,beta,dom,T) |
admproj(p, alpha, beta, dom, T) |
guesspoles(poles,n) |
guesspoles(poles, n_poles) |
r1range(r2,Ea,beta,sh) |
r1range(r2, Ea, beta, shifted) |
r2range(Ea,Eb,sh) |
r2range(Ea, Eb, shifted) |
coord2hilb(c,p) |
coord2hilb(coord, precision) |
hilb2coord(x,d,p) |
hilb2coord(x, dimensions, precision) |
val2bin(x,nf) |
val2bin(x, nfrac) |
bin2val(bi,bf) |
bin2val(bint, bfrac) |
k2ksi(sys,K,dK0) |
k2ksi(plant, K, dK0, T) |
go_par2k(coef) |
go_par2k(coef, ctx) |
f_sdh2p(coef) |
f_sdh2p(coef, ctx) |
f_sdl2p(coef) |
f_sdl2p(coef, ctx) |
go_sdh2p(x) |
go_sdh2p(x, ctx) |
go_sdl2p(x) |
go_sdl2p(x, ctx) |
randbeta(a,b,r,c) |
randbeta(a, b, rows, cols) |
randgamma(a,r,c) |
randgamma(a, rows, cols) |
sa_testfun(x) |
sa_testfun(x) |
updateopt(opt,new) |
updateopt(options, opt) |
uniproj(x) |
uniproj(x) |
u2range(u,lo,hi) |
u2range(u, lo, hi) |
sdnorm(sys,K,'l1') |
sdl1norm(plant, K, T) — L1/peak-to-peak norm analysis |
| (no MATLAB equivalent) | sdl1_reg(P_lifted, N_fir) — L1-optimal synthesis |
| (no MATLAB equivalent) | sd_mixed_h2_l1(P_lifted, N_fir, l1_bound=…) — mixed H2/L1 |
| (no MATLAB equivalent) | sd_constrained(P_lifted, N_fir, envelope=(lo,hi)) — hard constraints |
| (no MATLAB equivalent) | youla_basis(P_lifted, K0, N_fir) — raw Youla maps for custom problems |
Key design decisions
Lifting is first-class. design/lifting.py implements the exact Van Loan
matrix-exponential method (Hagiwara-Araki 1995, Chen-Francis 1995).
Unlike simple ZOH discretization, the lifted plant accounts for inter-sample
signal energy — the gamma term captures what a continuous-time H2 design
recovers that a purely discrete design misses.
No frequency gridding for norms. All norm functions in analysis/norms.py
use Lyapunov equations or Hamiltonian bisection. This eliminates
BadCoefficients warnings from ill-conditioned transfer functions and gives
machine-precision results even for systems with integrators or sharp resonances.
SVD-based Diophantine solver. dioph() uses scipy.linalg.lstsq instead
of a custom QR solver, making it robust to rank-deficient Sylvester matrices
that arise when the plant has repeated or near-common poles.
Convex synthesis goes beyond the MATLAB toolbox. design/convex.py
implements L1 (peak-to-peak) optimal control and mixed H2/L1 design via the
Youla Q-parameterisation — a capability not present in the original MATLAB
DirectSD toolbox. All stabilising controllers are parameterised as
K = K0 + Δ(Q) where Q is a stable FIR sequence, reducing the synthesis
to a Linear Programme solved by CVXPY. This is an optional dependency:
the rest of the package works without it.
Notes
- Simulink
.mdlfiles (dsdmodel/) have no Python equivalent; usescipy.signal.lsimfor simulation. - The
@lti,@tf,@zpkMATLAB overrides are replaced throughout byscipy.signal. - All functions accept
scipy.signal.lti,scipy.signal.dlti,(num, den)tuples, andscipy.signal.StateSpaceobjects interchangeably.
How to Cite
If you use this package in your research, please cite it — see
CITATION.cff, or use GitHub's "Cite this repository" button
on the repo page.
License
This Python port is licensed under the BSD 3-Clause License © 2026 Roozbeh Dargahi. The original MATLAB toolbox is © 1995–2006 Konstantin Polyakov; this license covers only the Python implementation in this repository, not the original MATLAB source.
Project details
Release history Release notifications | RSS feed
Download files
Download the file for your platform. If you're not sure which to choose, learn more about installing packages.
Source Distribution
Built Distribution
Filter files by name, interpreter, ABI, and platform.
If you're not sure about the file name format, learn more about wheel file names.
Copy a direct link to the current filters
File details
Details for the file directsd_python-0.2.0.tar.gz.
File metadata
- Download URL: directsd_python-0.2.0.tar.gz
- Upload date:
- Size: 220.2 kB
- Tags: Source
- Uploaded using Trusted Publishing? Yes
- Uploaded via: twine/6.1.0 CPython/3.13.12
File hashes
| Algorithm | Hash digest | |
|---|---|---|
| SHA256 |
25ad642c0e745ee8712b7a7e91a00e0d1df93f482776283035a99de9bfed6da8
|
|
| MD5 |
fb900ef33fad9dd5200e2aec8e3a509a
|
|
| BLAKE2b-256 |
9182fa49a6d5edd0fd921859845b72d5fd16de325e35468b14bda4474373fa58
|
Provenance
The following attestation bundles were made for directsd_python-0.2.0.tar.gz:
Publisher:
publish.yml on RooDK0x7b4/DirectSD-Python
-
Statement:
-
Statement type:
https://in-toto.io/Statement/v1 -
Predicate type:
https://docs.pypi.org/attestations/publish/v1 -
Subject name:
directsd_python-0.2.0.tar.gz -
Subject digest:
25ad642c0e745ee8712b7a7e91a00e0d1df93f482776283035a99de9bfed6da8 - Sigstore transparency entry: 2194656897
- Sigstore integration time:
-
Permalink:
RooDK0x7b4/DirectSD-Python@b55f188c300a00a30a30874b3b1eb2a1a2f7a09d -
Branch / Tag:
refs/tags/v0.2.0 - Owner: https://github.com/RooDK0x7b4
-
Access:
public
-
Token Issuer:
https://token.actions.githubusercontent.com -
Runner Environment:
github-hosted -
Publication workflow:
publish.yml@b55f188c300a00a30a30874b3b1eb2a1a2f7a09d -
Trigger Event:
release
-
Statement type:
File details
Details for the file directsd_python-0.2.0-py3-none-any.whl.
File metadata
- Download URL: directsd_python-0.2.0-py3-none-any.whl
- Upload date:
- Size: 231.7 kB
- Tags: Python 3
- Uploaded using Trusted Publishing? Yes
- Uploaded via: twine/6.1.0 CPython/3.13.12
File hashes
| Algorithm | Hash digest | |
|---|---|---|
| SHA256 |
d0ea049126d8772aa26bb2bcb6c2860bbcb9dd10f547aa5ea6c6e4dc7d00d125
|
|
| MD5 |
72600c67e73f054ed3374837feaf6c29
|
|
| BLAKE2b-256 |
1da6197500e2096737294a560ab48310f0a047f7b518cda4d04331ab61d62a5b
|
Provenance
The following attestation bundles were made for directsd_python-0.2.0-py3-none-any.whl:
Publisher:
publish.yml on RooDK0x7b4/DirectSD-Python
-
Statement:
-
Statement type:
https://in-toto.io/Statement/v1 -
Predicate type:
https://docs.pypi.org/attestations/publish/v1 -
Subject name:
directsd_python-0.2.0-py3-none-any.whl -
Subject digest:
d0ea049126d8772aa26bb2bcb6c2860bbcb9dd10f547aa5ea6c6e4dc7d00d125 - Sigstore transparency entry: 2194656901
- Sigstore integration time:
-
Permalink:
RooDK0x7b4/DirectSD-Python@b55f188c300a00a30a30874b3b1eb2a1a2f7a09d -
Branch / Tag:
refs/tags/v0.2.0 - Owner: https://github.com/RooDK0x7b4
-
Access:
public
-
Token Issuer:
https://token.actions.githubusercontent.com -
Runner Environment:
github-hosted -
Publication workflow:
publish.yml@b55f188c300a00a30a30874b3b1eb2a1a2f7a09d -
Trigger Event:
release
-
Statement type: