Skip to main content

Direct Sampled-Data Control Systems Toolbox — Python port of MATLAB DirectSD v3.0

Project description

DirectSD — Python Package

PyPI version Python versions License

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 .mdl files (dsdmodel/) have no Python equivalent; use scipy.signal.lsim for simulation.
  • The @lti, @tf, @zpk MATLAB overrides are replaced throughout by scipy.signal.
  • All functions accept scipy.signal.lti, scipy.signal.dlti, (num, den) tuples, and scipy.signal.StateSpace objects 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


Download files

Download the file for your platform. If you're not sure which to choose, learn more about installing packages.

Source Distribution

directsd_python-0.2.0.tar.gz (220.2 kB view details)

Uploaded Source

Built Distribution

If you're not sure about the file name format, learn more about wheel file names.

directsd_python-0.2.0-py3-none-any.whl (231.7 kB view details)

Uploaded Python 3

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

Hashes for directsd_python-0.2.0.tar.gz
Algorithm Hash digest
SHA256 25ad642c0e745ee8712b7a7e91a00e0d1df93f482776283035a99de9bfed6da8
MD5 fb900ef33fad9dd5200e2aec8e3a509a
BLAKE2b-256 9182fa49a6d5edd0fd921859845b72d5fd16de325e35468b14bda4474373fa58

See more details on using hashes here.

Provenance

The following attestation bundles were made for directsd_python-0.2.0.tar.gz:

Publisher: publish.yml on RooDK0x7b4/DirectSD-Python

Attestations: Values shown here reflect the state when the release was signed and may no longer be current.

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

Hashes for directsd_python-0.2.0-py3-none-any.whl
Algorithm Hash digest
SHA256 d0ea049126d8772aa26bb2bcb6c2860bbcb9dd10f547aa5ea6c6e4dc7d00d125
MD5 72600c67e73f054ed3374837feaf6c29
BLAKE2b-256 1da6197500e2096737294a560ab48310f0a047f7b518cda4d04331ab61d62a5b

See more details on using hashes here.

Provenance

The following attestation bundles were made for directsd_python-0.2.0-py3-none-any.whl:

Publisher: publish.yml on RooDK0x7b4/DirectSD-Python

Attestations: Values shown here reflect the state when the release was signed and may no longer be current.

Supported by

AWS Cloud computing and Security Sponsor Datadog Monitoring Depot Continuous Integration Fastly CDN Google Download Analytics Pingdom Monitoring Sentry Error logging StatusPage Status page