Skip to content

Commit fa8a516

Browse files
authored
Merge pull request #19 from jamestjsp/copilot/review-production-readiness-package
Assessing package for production readiness
2 parents 34d6c7b + ac01800 commit fa8a516

6 files changed

Lines changed: 113 additions & 10 deletions

File tree

README.md

Lines changed: 11 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -10,11 +10,20 @@ Supports continuous and discrete LTI models with MIMO capability.
1010
go get github.com/jamestjsp/controlsys
1111
```
1212

13-
> **Note:** This package depends on a [gonum fork](https://github.com/jamestjsp/gonum) for additional LAPACK routines. Add this to your `go.mod`:
13+
> **Note:** This package depends on a [gonum fork](https://github.com/jamestjsp/gonum) for additional LAPACK routines. Because `replace` directives do not propagate to downstream modules, applications that import `controlsys` must add this to their own `go.mod`:
1414
> ```
1515
> replace gonum.org/v1/gonum => github.com/jamestjsp/gonum v0.17.2-fork
1616
> ```
1717
18+
## Production Readiness
19+
20+
This package is intended to be usable in production control and estimation code, with the usual caveat that numerical software still needs application-specific validation.
21+
22+
- Pin both `controlsys` and the required gonum fork to explicit versions.
23+
- Validate mission-critical models against an external reference, especially for ill-conditioned realizations and delay-heavy systems.
24+
- `System` values are mutable. Use `Copy` before sharing a model across goroutines that may mutate names, delays, notes, or other receiver state.
25+
- The repository CI runs `go vet ./...` and `go test -v -count=1 -race ./...`; those are the recommended baseline checks for downstream integrations.
26+
1827
## Features
1928
2029
- **Three representations:** state-space, transfer function, zero-pole-gain (ZPK) with bidirectional conversion
@@ -107,7 +116,7 @@ func main() {
107116
| `Dlqr` | Discrete-time LQR regulator |
108117
| `Lqe` | Kalman filter (observer) gain |
109118
| `Lqi` | LQR with integral action |
110-
| `Pole` | Pole placement |
119+
| `Place` | Pole placement |
111120
| `Care` | Continuous algebraic Riccati equation |
112121
| `Dare` | Discrete algebraic Riccati equation |
113122

delay.go

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -26,7 +26,7 @@ func (sys *System) SetDelay(delay *mat.Dense) error {
2626
if err := validateDelay(delay, p, m, sys.Dt); err != nil {
2727
return err
2828
}
29-
sys.Delay = delay
29+
sys.Delay = copyDelayOrNil(delay)
3030
return nil
3131
}
3232

@@ -1643,7 +1643,7 @@ func (sys *System) GetDelayModel() (H *System, tau []float64) {
16431643
// Pull all delays into LFT
16441644
lft, err := sys.PullDelaysToLFT()
16451645
if err != nil {
1646-
// If PullDelaysToLFT fails (e.g. non-decomposable residual),
1646+
// If PullDelaysToLFT fails (e.g. non-decomposable residual),
16471647
// we fallback to just extracting InternalDelay if it exists,
16481648
// or return the original system.
16491649
if sys.LFT == nil {

delay_test.go

Lines changed: 34 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -29,6 +29,26 @@ func TestNewWithDelay(t *testing.T) {
2929
}
3030
}
3131

32+
func TestNewWithDelayCopiesDelayMatrix(t *testing.T) {
33+
delay := mat.NewDense(1, 1, []float64{3})
34+
sys, err := NewWithDelay(
35+
mat.NewDense(1, 1, []float64{0.5}),
36+
mat.NewDense(1, 1, []float64{1}),
37+
mat.NewDense(1, 1, []float64{1}),
38+
mat.NewDense(1, 1, []float64{0}),
39+
delay,
40+
1.0,
41+
)
42+
if err != nil {
43+
t.Fatal(err)
44+
}
45+
46+
delay.Set(0, 0, 99)
47+
if got := sys.Delay.At(0, 0); got != 3 {
48+
t.Fatalf("delay alias detected: got %v, want 3", got)
49+
}
50+
}
51+
3252
func TestNewWithDelayNil(t *testing.T) {
3353
A := mat.NewDense(1, 1, []float64{0.5})
3454
B := mat.NewDense(1, 1, []float64{1})
@@ -62,6 +82,20 @@ func TestSetDelayNegative(t *testing.T) {
6282
}
6383
}
6484

85+
func TestSetDelayCopiesInputMatrix(t *testing.T) {
86+
sys, _ := NewFromSlices(1, 1, 1,
87+
[]float64{0.5}, []float64{1}, []float64{1}, []float64{0}, 1.0)
88+
delay := mat.NewDense(1, 1, []float64{2})
89+
if err := sys.SetDelay(delay); err != nil {
90+
t.Fatal(err)
91+
}
92+
93+
delay.Set(0, 0, 99)
94+
if got := sys.Delay.At(0, 0); got != 2 {
95+
t.Fatalf("delay alias detected: got %v, want 2", got)
96+
}
97+
}
98+
6599
func TestSetDelayFractionalDiscrete(t *testing.T) {
66100
sys, _ := NewFromSlices(1, 1, 1,
67101
[]float64{0.5}, []float64{1}, []float64{1}, []float64{0}, 1.0)

doc.go

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -3,4 +3,8 @@
33
// transport delays, state-space and transfer-function representations,
44
// frequency response analysis, discretization, simulation, and
55
// model reduction.
6+
//
7+
// Methods that configure delays, names, or notes mutate the receiver,
8+
// so shared systems should be copied with Copy before concurrent
9+
// mutation.
610
package controlsys

ss.go

Lines changed: 4 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -10,7 +10,7 @@ import (
1010
// LFTDelay holds the internal delay representation using a linear
1111
// fractional transformation (LFT) structure.
1212
type LFTDelay struct {
13-
Tau []float64
13+
Tau []float64
1414
B2, C2, D12, D21, D22 *mat.Dense
1515
}
1616

@@ -189,11 +189,12 @@ func NewGain(D *mat.Dense, dt float64) (*System, error) {
189189
if D == nil {
190190
return nil, fmt.Errorf("D matrix required for gain system: %w", ErrDimensionMismatch)
191191
}
192+
p, m := D.Dims()
192193
return &System{
193194
A: &mat.Dense{},
194195
B: &mat.Dense{},
195196
C: &mat.Dense{},
196-
D: D,
197+
D: denseCopySafe(D, p, m),
197198
Dt: dt,
198199
}, nil
199200
}
@@ -241,10 +242,7 @@ func NewFromSlices(n, m, p int, a, b, c, d []float64, dt float64) (*System, erro
241242
} else {
242243
Dm = &mat.Dense{}
243244
}
244-
return &System{
245-
A: &mat.Dense{}, B: &mat.Dense{}, C: &mat.Dense{},
246-
D: Dm, Dt: dt,
247-
}, nil
245+
return NewGain(Dm, dt)
248246
}
249247

250248
return newNoCopy(A, B, C, D, dt)

ss_test.go

Lines changed: 58 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -98,6 +98,36 @@ func TestNewGain(t *testing.T) {
9898
}
9999
}
100100

101+
func TestNewCopiesInputMatrices(t *testing.T) {
102+
A := mat.NewDense(2, 2, []float64{0, 1, -2, -3})
103+
B := mat.NewDense(2, 1, []float64{0, 1})
104+
C := mat.NewDense(1, 2, []float64{1, 0})
105+
D := mat.NewDense(1, 1, []float64{0})
106+
107+
sys, err := New(A, B, C, D, 0)
108+
if err != nil {
109+
t.Fatal(err)
110+
}
111+
112+
A.Set(0, 0, 99)
113+
B.Set(0, 0, 99)
114+
C.Set(0, 0, 99)
115+
D.Set(0, 0, 99)
116+
117+
if got := sys.A.At(0, 0); got != 0 {
118+
t.Fatalf("A alias detected: got %v, want 0", got)
119+
}
120+
if got := sys.B.At(0, 0); got != 0 {
121+
t.Fatalf("B alias detected: got %v, want 0", got)
122+
}
123+
if got := sys.C.At(0, 0); got != 1 {
124+
t.Fatalf("C alias detected: got %v, want 1", got)
125+
}
126+
if got := sys.D.At(0, 0); got != 0 {
127+
t.Fatalf("D alias detected: got %v, want 0", got)
128+
}
129+
}
130+
101131
func TestNewFromSlices(t *testing.T) {
102132
sys, err := NewFromSlices(2, 1, 1,
103133
[]float64{0, 1, -2, -3},
@@ -126,6 +156,34 @@ func TestNewFromSlicesGain(t *testing.T) {
126156
}
127157
}
128158

159+
func TestNewGainCopiesInputMatrix(t *testing.T) {
160+
D := mat.NewDense(1, 2, []float64{3, 4})
161+
162+
sys, err := NewGain(D, 0)
163+
if err != nil {
164+
t.Fatal(err)
165+
}
166+
167+
D.Set(0, 0, 99)
168+
if got := sys.D.At(0, 0); got != 3 {
169+
t.Fatalf("D alias detected: got %v, want 3", got)
170+
}
171+
}
172+
173+
func TestNewFromSlicesGainOnlyCopiesInputSlice(t *testing.T) {
174+
d := []float64{3, 4}
175+
176+
sys, err := NewFromSlices(0, 2, 1, nil, nil, nil, d, 0)
177+
if err != nil {
178+
t.Fatal(err)
179+
}
180+
181+
d[0] = 99
182+
if got := sys.D.At(0, 0); got != 3 {
183+
t.Fatalf("D alias detected: got %v, want 3", got)
184+
}
185+
}
186+
129187
func TestCopy(t *testing.T) {
130188
sys, _ := NewFromSlices(2, 1, 1,
131189
[]float64{0, 1, -2, -3},

0 commit comments

Comments
 (0)