Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
65 changes: 65 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
@@ -1,5 +1,70 @@
# Changelog

## 2026-04-07 — Classical Control Theory Analysis Stack

Adds frequency-domain and time-domain control analysis across four packages,
closing the gap between GDS structural specification and classical control
engineering workflows. Tracking issue: #198.

### gds-domains (symbolic layer)

- **`symbolic/transfer.py`** — transfer function representation and analysis
- `ss_to_tf()`: state-space to transfer function matrix (SISO and MIMO)
- `poles()`, `zeros()`: polynomial root-finding via SymPy
- `characteristic_polynomial()`: det(sI - A) as coefficient list
- `controllability_matrix()`, `observability_matrix()`: Kalman rank tests
- `is_controllable()`, `is_observable()`: rank-based checks
- `is_minimum_phase()`: RHP zero detection
- `sensitivity()`: Gang of Six (S, T, CS, PS, KS, KPS)
- `TransferFunction`, `TransferFunctionMatrix` data types
- **`symbolic/delay.py`** — time delay modeling
- `pade_approximation()`: (N,N) Padé approximant of e^{-sτ}
- `delay_system()`: cascade TF with Padé delay

### gds-analysis (numerical layer)

- **`linear.py`** — numerical linear systems analysis (requires `[continuous]`)
- `eigenvalues()`, `is_stable()`, `is_marginally_stable()`: stability checks
- `frequency_response()`: H(jω) magnitude/phase via scipy
- `gain_margin()`, `phase_margin()`: stability margin computation
- `discretize()`: continuous-to-discrete (Tustin, ZOH, Euler, backward Euler)
- `lqr()`, `dlqr()`: continuous/discrete Linear Quadratic Regulator
- `kalman()`: steady-state Kalman filter gain
- `gain_schedule()`: multi-point LQR across operating points
- **`response.py`** — step/impulse response and time-domain metrics
- `step_response()`, `impulse_response()`: state-space simulation via scipy
- `step_response_metrics()`: rise time, settling time, overshoot, SSE
- `StepMetrics` data type (no scipy needed for metric extraction)
- `metrics_from_ode_results()`: convenience wrapper for gds-continuous

### gds-proof (formal verification layer)

- **`analysis/lyapunov.py`** — Lyapunov stability proofs
- `lyapunov_candidate()`: verify V(x) > 0 and dV/dt < 0 (continuous/discrete)
- `quadratic_lyapunov()`: verify V = x'Px via A'P + PA eigenvalue check
- `find_quadratic_lyapunov()`: solve Lyapunov equation A'P + PA = -Q
- `passivity_certificate()`: verify dV/dt ≤ s(u, y) for dissipativity

### gds-viz (visualization layer)

- **`frequency.py`** — frequency response plots (requires `[control]`)
- `bode_plot()`: magnitude + phase with optional margin annotations
- `nyquist_plot()`: complex plane with unit circle and critical point
- `nichols_plot()`: open-loop phase vs. gain with M-circles
- `root_locus_plot()`: pole migration with gain sweep
- **`response.py`** — time-domain plots (requires `[control]`)
- `step_response_plot()`: with StepMetrics annotation overlays
- `impulse_response_plot()`: basic response plot
- `compare_responses()`: multi-response overlay
- New `[control]` optional extra (matplotlib + numpy, no gds-continuous)
- New `[plots]` extra (union of `[phase]` and `[control]`)

Zero new third-party dependencies. 104 new tests across all packages.

Closes #199, #200, #201, #202, #203, #204.

---

## 2026-04-07 — gds-proof Layer 1 Integration

Promotes gds-proof from a standalone package (Layer 0, protocol-only) to a
Expand Down
20 changes: 18 additions & 2 deletions CLAUDE.md
Original file line number Diff line number Diff line change
Expand Up @@ -14,13 +14,27 @@ This file provides guidance to Claude Code (claude.ai/code) when working with co
| gds-domains | `gds_domains.{stockflow,control,business,software,games,symbolic}` | `packages/gds-domains/` | All domain DSLs |
| gds-sim | `gds_sim` | `packages/gds-sim/` | Discrete-time simulation |
| gds-continuous | `gds_continuous` | `packages/gds-continuous/` | Continuous-time ODE engine |
| gds-analysis | `gds_analysis`, `gds_analysis.psuu` | `packages/gds-analysis/` | Spec-to-sim bridge + PSUU |
| gds-analysis | `gds_analysis`, `gds_analysis.psuu` | `packages/gds-analysis/` | Spec-to-sim bridge + PSUU + linear systems analysis |
| gds-interchange | `gds_interchange.owl` | `packages/gds-interchange/` | OWL/SHACL/SPARQL + future bridges |
| gds-viz | `gds_viz` | `packages/gds-viz/` | Mermaid + phase portraits |
| gds-viz | `gds_viz` | `packages/gds-viz/` | Mermaid + phase portraits + Bode/Nyquist/root locus |
| gds-examples | — | `packages/gds-examples/` | Tutorials + examples |

Deprecated shim packages (v0.99.0, re-export with DeprecationWarning): gds-owl, gds-psuu, gds-stockflow, gds-control, gds-games, gds-software, gds-business, gds-symbolic.

## What GDS Is Not

GDS is a **specification and verification** framework, not a simulation engine or general-purpose modeling library. If you have used SimPy, Mesa, cadCAD, or SciPy before, read this section before writing any code.

| Misconception | Reality |
|--------------|---------|
| GDS is a simulation engine | GDS is a **specification** layer. Do not write `for t in range(...)` loops. Declare a spec; `gds-sim` or `gds-continuous` executes it. |
| GDS is SimPy / Mesa / cadCAD | GDS is a typed composition algebra with formal verification. The closest analogy is a specification language (like Alloy or TLA+), not an ABM or process-simulation framework. |
| GDS is SciPy.integrate | `gds-continuous` wraps SciPy internally, but users declare ODE systems via specs, not `solve_ivp()` calls directly. |
| Blocks are classes you instantiate freely | Block roles (`BoundaryAction`, `Policy`, `Mechanism`, `ControlAction`) are the four leaf types. Domain DSLs compile *models* into blocks via `compile_model()` / `compile_to_system()`. Hand-wiring blocks is for framework extension, not typical usage. |
| Verification is type checking | Verification operates on compiled IR (`SystemIR`) and specs (`GDSSpec`). It checks structural topology (G-001..G-006) and semantic properties (SC-001..SC-009), not Python types at runtime. |
| Parameters get assigned values by GDS | `ParameterSchema` (Theta) is structural metadata only. GDS never binds parameter values. `gds-analysis` and its PSUU module handle sweep and evaluation. |
| Domain DSLs are interchangeable | Each DSL targets a specific modeling paradigm. Use `gds_domains.stockflow` for conservation-law systems, `.control` for state-space controllers, `.games` for strategic interaction, `.business` for causal/value-stream diagrams, `.software` for architecture diagrams. See the [Choosing a DSL](https://blockscience.github.io/gds-core/guides/choosing-a-dsl/) guide. |

## Commands

```bash
Expand Down Expand Up @@ -107,6 +121,8 @@ The composition tree follows a convergent tiered pattern:

`gds_domains.games` is more complex — it subclasses `AtomicBlock` as `OpenGame` with its own IR (`PatternIR`), but projects back to `SystemIR` via `PatternIR.to_system_ir()`.

`gds_domains.symbolic` extends the control DSL with SymPy-based ODEs, linearization (A, B, C, D matrices), transfer functions, poles/zeros, controllability/observability, Padé time delay approximation, and the Gang of Six sensitivity functions. This is the entry point to the classical control theory analysis stack — `LinearizedSystem` flows into `gds-analysis` for numerical analysis and `gds-viz` for frequency-domain plots.

### Two Type Systems

These coexist at different levels:
Expand Down
166 changes: 0 additions & 166 deletions improvement-plans/review_synthesis.md

This file was deleted.

20 changes: 19 additions & 1 deletion packages/gds-analysis/CLAUDE.md
Original file line number Diff line number Diff line change
Expand Up @@ -10,7 +10,7 @@

## Architecture

Five modules, each bridging one aspect of structural specification to runtime:
Seven modules bridging structural specification to runtime analysis:

| Module | Function | Paper reference |
|--------|----------|-----------------|
Expand All @@ -19,6 +19,8 @@ Five modules, each bridging one aspect of structural specification to runtime:
| `metrics.py` | `trajectory_distances(results, spec)` → distance matrix | Assumption 3.2 |
| `reachability.py` | `reachable_set(model, state, inputs)` → R(x) | Def 4.1, 4.2 |
| `backward_reachability.py` | `backward_reachable_set(dynamics, ...)` → B(T) | Def 4.1 (backward) |
| `linear.py` | Eigenvalue stability, frequency response, margins, discretization, LQR, Kalman | `[continuous]` |
| `response.py` | Step/impulse response computation + time-domain metrics (StepMetrics) | `[continuous]` |

### spec_to_model adapter

Expand All @@ -35,6 +37,22 @@ If `enforce_constraints=True`, wraps BoundaryAction policies with `guarded_polic
- `reachable_graph(spec, model, states, input_samples)` — builds full reachability graph across multiple states
- `configuration_space(reachability_graph)` — extracts largest SCC (the configuration space X_C)

### Linear systems analysis (`linear.py`, requires `[continuous]`)

All functions accept `list[list[float]]` matrices (matching `LinearizedSystem` fields):
- `eigenvalues(A)`, `is_stable(A)`, `is_marginally_stable(A)` — stability checks
- `frequency_response(A, B, C, D, omega)` → `(omega, mag_db, phase_deg)`
- `gain_margin(num, den)`, `phase_margin(num, den)` — stability margins
- `discretize(A, B, C, D, dt, method)` → `(Ad, Bd, Cd, Dd)` via scipy.signal
- `lqr(A, B, Q, R)`, `dlqr(Ad, Bd, Q, R)` → `(K, P, E)` gain + Riccati + eigenvalues
- `kalman(A, C, Q, R)` → `(L, P)` observer gain + covariance
- `gain_schedule(linearize_fn, points, Q, R)` → gains at multiple operating points

### Response metrics (`response.py`)

- `step_response_metrics(times, values, setpoint)` → `StepMetrics` (no scipy needed)
- `step_response(A, B, C, D)`, `impulse_response(A, B, C, D)` — scipy-based simulation

## Commands

```bash
Expand Down
44 changes: 42 additions & 2 deletions packages/gds-analysis/gds_analysis/__init__.py
Original file line number Diff line number Diff line change
@@ -1,8 +1,8 @@
"""Dynamical analysis for GDS specifications.

Bridges gds-framework structural annotations to gds-sim runtime,
enabling constraint enforcement, metric computation, and reachability
analysis on concrete trajectories.
enabling constraint enforcement, metric computation, reachability
analysis, linear systems analysis, and response metrics.
"""

__version__ = "0.1.2"
Expand All @@ -22,17 +22,57 @@
reachable_graph,
reachable_set,
)
from gds_analysis.response import StepMetrics, step_response_metrics

__all__ = [
"BackwardReachableSet",
"Isochrone",
"ReachabilityResult",
"StepMetrics",
"backward_reachable_set",
"configuration_space",
"extract_isochrones",
"guarded_policy",
"reachable_graph",
"reachable_set",
"spec_to_model",
"step_response_metrics",
"trajectory_distances",
]


# Lazy imports for optional modules (require [continuous] extra)
_LINEAR_EXPORTS = {
"eigenvalues",
"is_stable",
"is_marginally_stable",
"frequency_response",
"gain_margin",
"phase_margin",
"discretize",
"lqr",
"dlqr",
"kalman",
"gain_schedule",
}

_RESPONSE_SCIPY_EXPORTS = {
"step_response",
"impulse_response",
"metrics_from_ode_results",
}


def __getattr__(name: str) -> object:
"""Lazy import for optional scipy-dependent functions."""
if name in _LINEAR_EXPORTS:
from gds_analysis import linear

return getattr(linear, name)

if name in _RESPONSE_SCIPY_EXPORTS:
from gds_analysis import response

return getattr(response, name)

raise AttributeError(f"module 'gds_analysis' has no attribute {name!r}")
Loading