diff --git a/README.md b/README.md index ea7db08..0dc294a 100644 --- a/README.md +++ b/README.md @@ -312,7 +312,7 @@ func main() { | `Series` | Cascade connection | | `Parallel` | Sum connection | | `Feedback` | Closed-loop with feedback | -| `SafeFeedback` | Feedback with automatic delay handling | +| `WithApproximatedDelays` / `WithPadeOrder` / `WithThiranOrder` | Feedback options for a delay-free rational closed loop | | `Append` | Block diagonal concatenation | | `SumBlk` | Sum block from string expression | | `Connect` / `ConnectByName` | General interconnection by indices or signal names | diff --git a/architecture_prd103_test.go b/architecture_prd103_test.go index 629331d..1761d34 100644 --- a/architecture_prd103_test.go +++ b/architecture_prd103_test.go @@ -132,15 +132,15 @@ func TestPRD103DelayConversionPolicyPublicWorkflows(t *testing.T) { discPlant.InputDelay = []float64{2.5, 0} discPlant.OutputDelay = []float64{0, 3.5} discPlant.Delay = mat.NewDense(2, 2, []float64{0, 2.5, 3.5, 6.0}) - if _, err := SafeFeedback(discPlant, controller, -1); !errors.Is(err, ErrFractionalDelay) { - t.Fatalf("SafeFeedback fractional delay error = %v, want ErrFractionalDelay", err) + if _, err := Feedback(discPlant, controller, -1, WithApproximatedDelays()); !errors.Is(err, ErrFractionalDelay) { + t.Fatalf("Feedback fractional delay error = %v, want ErrFractionalDelay", err) } - closed, err := SafeFeedback(discPlant, controller, -1, WithThiranOrder(3)) + closed, err := Feedback(discPlant, controller, -1, WithThiranOrder(3)) if err != nil { t.Fatal(err) } if closed.HasDelay() { - t.Fatalf("SafeFeedback kept external delay: input=%v output=%v io=%v", closed.InputDelay, closed.OutputDelay, closed.Delay) + t.Fatalf("Feedback kept external delay: input=%v output=%v io=%v", closed.InputDelay, closed.OutputDelay, closed.Delay) } } diff --git a/architecture_prd95_test.go b/architecture_prd95_test.go index 6f29ecc..d4bf1d7 100644 --- a/architecture_prd95_test.go +++ b/architecture_prd95_test.go @@ -84,15 +84,15 @@ func TestPRD95DelayBankPublicWorkflowsShareRules(t *testing.T) { 4.5, 8.0, }) - if _, err := SafeFeedback(discPlant, controller, -1); !errors.Is(err, ErrFractionalDelay) { - t.Fatalf("SafeFeedback without Thiran err = %v, want ErrFractionalDelay", err) + if _, err := Feedback(discPlant, controller, -1, WithApproximatedDelays()); !errors.Is(err, ErrFractionalDelay) { + t.Fatalf("Feedback without Thiran err = %v, want ErrFractionalDelay", err) } - closed, err := SafeFeedback(discPlant, controller, -1, WithThiranOrder(3)) + closed, err := Feedback(discPlant, controller, -1, WithThiranOrder(3)) if err != nil { t.Fatal(err) } if closed.HasDelay() { - t.Fatalf("SafeFeedback kept external delay: input=%v output=%v io=%v", closed.InputDelay, closed.OutputDelay, closed.Delay) + t.Fatalf("Feedback kept external delay: input=%v output=%v io=%v", closed.InputDelay, closed.OutputDelay, closed.Delay) } } @@ -106,7 +106,7 @@ func TestPRD95DelayBankKeepsIntegerDelayExactWithThiranOrder(t *testing.T) { t.Fatal(err) } - closed, err := SafeFeedback(sys, controller, -1, WithThiranOrder(3)) + closed, err := Feedback(sys, controller, -1, WithThiranOrder(3)) if err != nil { t.Fatal(err) } diff --git a/architecture_remaining_test.go b/architecture_remaining_test.go index f2d13c6..57eac6b 100644 --- a/architecture_remaining_test.go +++ b/architecture_remaining_test.go @@ -81,7 +81,7 @@ func TestRemainingArchitectureDelayTopologyPublicOperations(t *testing.T) { if err != nil { t.Fatal(err) } - safeSplit, err := SafeFeedback(split, controller, -1, WithPadeOrder(2)) + safeSplit, err := Feedback(split, controller, -1, WithPadeOrder(2)) if err != nil { t.Fatal(err) } diff --git a/bench_test.go b/bench_test.go index 9749528..2e59110 100644 --- a/bench_test.go +++ b/bench_test.go @@ -540,14 +540,14 @@ func BenchmarkDiscretizeWithOpts_IODelayThiran(b *testing.B) { } } -func BenchmarkSafeFeedback(b *testing.B) { +func BenchmarkFeedbackApproximatedDelays(b *testing.B) { plant := benchSys(10, 3, 3) plant.Dt = 1.0 ctrl := benchSys(5, 3, 3) ctrl.Dt = 1.0 b.ResetTimer() for i := 0; i < b.N; i++ { - SafeFeedback(plant, ctrl, -1) + Feedback(plant, ctrl, -1, WithApproximatedDelays()) } } diff --git a/connect.go b/connect.go index 3873442..b13d238 100644 --- a/connect.go +++ b/connect.go @@ -382,7 +382,13 @@ func sliceOrZeros(s []float64, n int) []float64 { return make([]float64, n) } -func Feedback(plant, controller *System, sign float64) (*System, error) { +// Feedback returns the closed-loop model of plant with controller in the +// feedback path. sign is -1 for negative feedback, +1 for positive; a nil +// controller closes unit feedback. The result is exact by default: delays +// are carried as internal delays when the loop topology supports them. +// Pass WithApproximatedDelays, WithPadeOrder, or WithThiranOrder to receive +// a delay-free rational model instead. +func Feedback(plant, controller *System, sign float64, opts ...FeedbackOption) (*System, error) { if plant == nil { return nil, fmt.Errorf("feedback: plant cannot be nil") } @@ -404,6 +410,9 @@ func Feedback(plant, controller *System, sign float64) (*System, error) { if err := domainMatch(plant, controller); err != nil { return nil, err } + if cfg := newFeedbackConfig(opts); cfg.approximateDelays { + return feedbackWithApproximatedDelays(plant, controller, sign, cfg) + } n1, m1, p1 := plant.Dims() n2, m2, p2 := controller.Dims() if p1 != m2 { diff --git a/connect_test.go b/connect_test.go index 18e7889..6d98019 100644 --- a/connect_test.go +++ b/connect_test.go @@ -847,7 +847,7 @@ func TestAppend_WithDelay(t *testing.T) { } } -func TestSafeFeedback_DiscreteInputDelay(t *testing.T) { +func TestFeedbackApprox_DiscreteInputDelay(t *testing.T) { plant, _ := New( mat.NewDense(1, 1, []float64{0.5}), mat.NewDense(1, 1, []float64{1}), @@ -858,7 +858,7 @@ func TestSafeFeedback_DiscreteInputDelay(t *testing.T) { _ = plant.SetInputDelay([]float64{3}) controller, _ := NewGain(mat.NewDense(1, 1, []float64{0.8}), 0.1) - cl, err := SafeFeedback(plant, controller, -1) + cl, err := Feedback(plant, controller, -1, WithApproximatedDelays()) if err != nil { t.Fatal(err) } @@ -883,11 +883,11 @@ func TestSafeFeedback_DiscreteInputDelay(t *testing.T) { clResp, _ := cl.Simulate(u, nil, nil) manualResp, _ := clManual.Simulate(u, nil, nil) if !matEqual(clResp.Y, manualResp.Y, 1e-10) { - t.Error("SafeFeedback != manual absorb+feedback") + t.Error("Feedback != manual absorb+feedback") } } -func TestSafeFeedback_DiscreteOutputDelay(t *testing.T) { +func TestFeedbackApprox_DiscreteOutputDelay(t *testing.T) { plant, _ := New( mat.NewDense(1, 1, []float64{0.5}), mat.NewDense(1, 1, []float64{1}), @@ -898,7 +898,7 @@ func TestSafeFeedback_DiscreteOutputDelay(t *testing.T) { _ = plant.SetOutputDelay([]float64{2}) controller, _ := NewGain(mat.NewDense(1, 1, []float64{0.5}), 0.1) - cl, err := SafeFeedback(plant, controller, -1) + cl, err := Feedback(plant, controller, -1, WithApproximatedDelays()) if err != nil { t.Fatal(err) } @@ -907,7 +907,7 @@ func TestSafeFeedback_DiscreteOutputDelay(t *testing.T) { } } -func TestSafeFeedback_DiscreteIODelay(t *testing.T) { +func TestFeedbackApprox_DiscreteIODelay(t *testing.T) { plant, _ := New( mat.NewDense(2, 2, []float64{0.8, 0.1, 0, 0.9}), mat.NewDense(2, 1, []float64{1, 0.5}), @@ -918,7 +918,7 @@ func TestSafeFeedback_DiscreteIODelay(t *testing.T) { plant.Delay = mat.NewDense(1, 1, []float64{4}) controller, _ := NewGain(mat.NewDense(1, 1, []float64{0.3}), 0.1) - cl, err := SafeFeedback(plant, controller, -1) + cl, err := Feedback(plant, controller, -1, WithApproximatedDelays()) if err != nil { t.Fatal(err) } @@ -927,7 +927,7 @@ func TestSafeFeedback_DiscreteIODelay(t *testing.T) { } } -func TestSafeFeedback_DiscreteControllerDelay(t *testing.T) { +func TestFeedbackApprox_DiscreteControllerDelay(t *testing.T) { plant, _ := New( mat.NewDense(1, 1, []float64{0.5}), mat.NewDense(1, 1, []float64{1}), @@ -944,7 +944,7 @@ func TestSafeFeedback_DiscreteControllerDelay(t *testing.T) { ) _ = controller.SetInputDelay([]float64{2}) - cl, err := SafeFeedback(plant, controller, -1) + cl, err := Feedback(plant, controller, -1, WithApproximatedDelays()) if err != nil { t.Fatal(err) } @@ -953,7 +953,7 @@ func TestSafeFeedback_DiscreteControllerDelay(t *testing.T) { } } -func TestSafeFeedback_NoDelay(t *testing.T) { +func TestFeedbackApprox_NoDelay(t *testing.T) { plant, _ := New( mat.NewDense(1, 1, []float64{0.5}), mat.NewDense(1, 1, []float64{1}), @@ -963,7 +963,7 @@ func TestSafeFeedback_NoDelay(t *testing.T) { ) controller, _ := NewGain(mat.NewDense(1, 1, []float64{0.8}), 0.1) - cl, err := SafeFeedback(plant, controller, -1) + cl, err := Feedback(plant, controller, -1, WithApproximatedDelays()) if err != nil { t.Fatal(err) } @@ -976,7 +976,7 @@ func TestSafeFeedback_NoDelay(t *testing.T) { } } -func TestSafeFeedback_ContinuousPade(t *testing.T) { +func TestFeedbackApprox_ContinuousPade(t *testing.T) { plant, _ := New( mat.NewDense(1, 1, []float64{-1}), mat.NewDense(1, 1, []float64{5}), @@ -987,7 +987,7 @@ func TestSafeFeedback_ContinuousPade(t *testing.T) { plant.Delay = mat.NewDense(1, 1, []float64{0.3}) controller, _ := NewGain(mat.NewDense(1, 1, []float64{0.8}), 0) - cl, err := SafeFeedback(plant, controller, -1, WithPadeOrder(3)) + cl, err := Feedback(plant, controller, -1, WithPadeOrder(3)) if err != nil { t.Fatal(err) } @@ -1016,7 +1016,7 @@ func TestSafeFeedback_ContinuousPade(t *testing.T) { } } -func TestSafeFeedback_ContinuousInputDelay(t *testing.T) { +func TestFeedbackApprox_ContinuousInputDelay(t *testing.T) { plant, _ := New( mat.NewDense(1, 1, []float64{-2}), mat.NewDense(1, 1, []float64{1}), @@ -1027,7 +1027,7 @@ func TestSafeFeedback_ContinuousInputDelay(t *testing.T) { _ = plant.SetInputDelay([]float64{0.5}) controller, _ := NewGain(mat.NewDense(1, 1, []float64{1}), 0) - cl, err := SafeFeedback(plant, controller, -1, WithPadeOrder(5)) + cl, err := Feedback(plant, controller, -1, WithPadeOrder(5)) if err != nil { t.Fatal(err) } @@ -1040,7 +1040,7 @@ func TestSafeFeedback_ContinuousInputDelay(t *testing.T) { } } -func TestSafeFeedback_ContinuousOutputDelay(t *testing.T) { +func TestFeedbackApprox_ContinuousOutputDelay(t *testing.T) { plant, _ := New( mat.NewDense(1, 1, []float64{-2}), mat.NewDense(1, 1, []float64{1}), @@ -1051,7 +1051,7 @@ func TestSafeFeedback_ContinuousOutputDelay(t *testing.T) { _ = plant.SetOutputDelay([]float64{0.4}) controller, _ := NewGain(mat.NewDense(1, 1, []float64{1}), 0) - cl, err := SafeFeedback(plant, controller, -1, WithPadeOrder(3)) + cl, err := Feedback(plant, controller, -1, WithPadeOrder(3)) if err != nil { t.Fatal(err) } @@ -1064,17 +1064,17 @@ func TestSafeFeedback_ContinuousOutputDelay(t *testing.T) { } } -func TestSafeFeedback_SingularAlgebraicLoop(t *testing.T) { +func TestFeedbackApprox_SingularAlgebraicLoop(t *testing.T) { plant, _ := NewGain(mat.NewDense(1, 1, []float64{1}), 0.1) controller, _ := NewGain(mat.NewDense(1, 1, []float64{1}), 0.1) - _, err := SafeFeedback(plant, controller, 1) + _, err := Feedback(plant, controller, 1, WithApproximatedDelays()) if !errors.Is(err, ErrSingularTransform) { t.Errorf("expected ErrSingularTransform, got %v", err) } } -func TestSafeFeedback_DomainMismatch(t *testing.T) { +func TestFeedbackApprox_DomainMismatch(t *testing.T) { plant, _ := New( mat.NewDense(1, 1, []float64{-1}), mat.NewDense(1, 1, []float64{1}), @@ -1084,13 +1084,13 @@ func TestSafeFeedback_DomainMismatch(t *testing.T) { ) controller, _ := NewGain(mat.NewDense(1, 1, []float64{1}), 0.1) - _, err := SafeFeedback(plant, controller, -1) + _, err := Feedback(plant, controller, -1, WithApproximatedDelays()) if !errors.Is(err, ErrDomainMismatch) { t.Errorf("expected ErrDomainMismatch, got %v", err) } } -func TestSafeFeedback_PositiveFeedback(t *testing.T) { +func TestFeedbackApprox_PositiveFeedback(t *testing.T) { plant, _ := New( mat.NewDense(1, 1, []float64{0.5}), mat.NewDense(1, 1, []float64{1}), @@ -1101,7 +1101,7 @@ func TestSafeFeedback_PositiveFeedback(t *testing.T) { _ = plant.SetInputDelay([]float64{2}) controller, _ := NewGain(mat.NewDense(1, 1, []float64{0.3}), 0.1) - cl, err := SafeFeedback(plant, controller, 1) + cl, err := Feedback(plant, controller, 1, WithApproximatedDelays()) if err != nil { t.Fatal(err) } @@ -1110,20 +1110,20 @@ func TestSafeFeedback_PositiveFeedback(t *testing.T) { } } -func TestSafeFeedback_WellPosednessCheck(t *testing.T) { +func TestFeedbackApprox_WellPosednessCheck(t *testing.T) { // Odd-order Pade has D=(-1)^N=-1. With negative feedback and unit gains: // I - (-1)*(-1)*1 = I - 1 = 0 → singular. plant, _ := NewGain(mat.NewDense(1, 1, []float64{1}), 0) plant.InputDelay = []float64{0.5} controller, _ := NewGain(mat.NewDense(1, 1, []float64{1}), 0) - _, err := SafeFeedback(plant, controller, -1, WithPadeOrder(3)) + _, err := Feedback(plant, controller, -1, WithPadeOrder(3)) if err == nil { t.Fatal("expected singular algebraic loop error for odd-order Pade with unit gains and negative feedback") } // Even-order Pade has D=1: I - (-1)*1*1 = 2 → not singular. - cl, err := SafeFeedback(plant, controller, -1, WithPadeOrder(2)) + cl, err := Feedback(plant, controller, -1, WithPadeOrder(2)) if err != nil { t.Fatalf("even-order Pade should work: %v", err) } @@ -1132,7 +1132,7 @@ func TestSafeFeedback_WellPosednessCheck(t *testing.T) { } } -func TestSafeFeedback_DefaultPadeOrder(t *testing.T) { +func TestFeedbackApprox_DefaultPadeOrder(t *testing.T) { plant, _ := New( mat.NewDense(1, 1, []float64{-1}), mat.NewDense(1, 1, []float64{1}), @@ -1143,7 +1143,7 @@ func TestSafeFeedback_DefaultPadeOrder(t *testing.T) { plant.Delay = mat.NewDense(1, 1, []float64{0.2}) controller, _ := NewGain(mat.NewDense(1, 1, []float64{0.5}), 0) - cl, err := SafeFeedback(plant, controller, -1) + cl, err := Feedback(plant, controller, -1, WithApproximatedDelays()) if err != nil { t.Fatal(err) } @@ -1153,7 +1153,7 @@ func TestSafeFeedback_DefaultPadeOrder(t *testing.T) { } } -func TestSafeFeedback_WithThiranOrder(t *testing.T) { +func TestFeedbackApprox_WithThiranOrder(t *testing.T) { plant, _ := New( mat.NewDense(1, 1, []float64{0.5}), mat.NewDense(1, 1, []float64{1}), @@ -1165,7 +1165,7 @@ func TestSafeFeedback_WithThiranOrder(t *testing.T) { controller, _ := NewGain(mat.NewDense(1, 1, []float64{0.5}), 0.1) - cl, err := SafeFeedback(plant, controller, -1, WithThiranOrder(2)) + cl, err := Feedback(plant, controller, -1, WithThiranOrder(2)) if err != nil { t.Fatal(err) } @@ -1179,7 +1179,7 @@ func TestSafeFeedback_WithThiranOrder(t *testing.T) { } } -func TestSafeFeedback_DiscreteWithThiranOrder(t *testing.T) { +func TestFeedbackApprox_DiscreteWithThiranOrder(t *testing.T) { plant, _ := New( mat.NewDense(1, 1, []float64{0.9}), mat.NewDense(1, 1, []float64{1}), @@ -1191,7 +1191,7 @@ func TestSafeFeedback_DiscreteWithThiranOrder(t *testing.T) { controller, _ := NewGain(mat.NewDense(1, 1, []float64{0.1}), 0.1) - cl, err := SafeFeedback(plant, controller, -1, WithThiranOrder(3)) + cl, err := Feedback(plant, controller, -1, WithThiranOrder(3)) if err != nil { t.Fatal(err) } @@ -1205,7 +1205,7 @@ func TestSafeFeedback_DiscreteWithThiranOrder(t *testing.T) { } } -func TestSafeFeedback_DiscreteFractionalDelayRequiresThiranOrder(t *testing.T) { +func TestFeedbackApprox_DiscreteFractionalDelayRequiresThiranOrder(t *testing.T) { plant, _ := New( mat.NewDense(1, 1, []float64{0.7}), mat.NewDense(1, 1, []float64{1}), @@ -1216,11 +1216,11 @@ func TestSafeFeedback_DiscreteFractionalDelayRequiresThiranOrder(t *testing.T) { plant.InputDelay = []float64{3.5} controller, _ := NewGain(mat.NewDense(1, 1, []float64{0.2}), 0.1) - if _, err := SafeFeedback(plant, controller, -1); !errors.Is(err, ErrFractionalDelay) { - t.Fatalf("SafeFeedback fractional delay error = %v, want ErrFractionalDelay", err) + if _, err := Feedback(plant, controller, -1, WithApproximatedDelays()); !errors.Is(err, ErrFractionalDelay) { + t.Fatalf("Feedback fractional delay error = %v, want ErrFractionalDelay", err) } - cl, err := SafeFeedback(plant, controller, -1, WithThiranOrder(3)) + cl, err := Feedback(plant, controller, -1, WithThiranOrder(3)) if err != nil { t.Fatal(err) } @@ -1233,7 +1233,7 @@ func TestSafeFeedback_DiscreteFractionalDelayRequiresThiranOrder(t *testing.T) { } } -func TestSafeFeedback_DiscreteThiranRejectsResidualIODelay(t *testing.T) { +func TestFeedbackApprox_DiscreteThiranRejectsResidualIODelay(t *testing.T) { plant, _ := New( mat.NewDense(2, 2, []float64{0.7, 0.2, -0.1, 0.6}), mat.NewDense(2, 2, []float64{1, 0, 0, 1}), @@ -1247,12 +1247,12 @@ func TestSafeFeedback_DiscreteThiranRejectsResidualIODelay(t *testing.T) { }) controller, _ := NewGain(mat.NewDense(2, 2, []float64{0.1, 0, 0, 0.2}), 0.1) - if _, err := SafeFeedback(plant, controller, -1, WithThiranOrder(2)); !errors.Is(err, ErrFeedbackDelay) { - t.Fatalf("SafeFeedback residual IODelay error = %v, want ErrFeedbackDelay", err) + if _, err := Feedback(plant, controller, -1, WithThiranOrder(2)); !errors.Is(err, ErrFeedbackDelay) { + t.Fatalf("Feedback residual IODelay error = %v, want ErrFeedbackDelay", err) } } -func TestSafeFeedback_ContinuousMIMO(t *testing.T) { +func TestFeedbackApprox_ContinuousMIMO(t *testing.T) { plant, _ := New( mat.NewDense(2, 2, []float64{-1, 0.5, 0, -2}), mat.NewDense(2, 2, []float64{1, 0, 0, 1}), @@ -1269,7 +1269,7 @@ func TestSafeFeedback_ContinuousMIMO(t *testing.T) { 0, ) - cl, err := SafeFeedback(plant, controller, -1, WithPadeOrder(3)) + cl, err := Feedback(plant, controller, -1, WithPadeOrder(3)) if err != nil { t.Fatal(err) } diff --git a/docs/api-mutation-audit.md b/docs/api-mutation-audit.md index 9ba006b..f77a6d2 100644 --- a/docs/api-mutation-audit.md +++ b/docs/api-mutation-audit.md @@ -83,9 +83,8 @@ ownership clarity, and release readiness, not shrinking the toolbox shape. | `ConnectByName` | `view-in`, `returns-mutable` | Uses named signals and builds through `BlkDiag`/`Connect`. | | `Inv` | `pure`, `returns-mutable` | Returns inverse model. | | `LFT` | `pure`, `returns-mutable` | Builds LFT model or visible extraction. | -| `SafeFeedback` | `pure`, `returns-mutable` | Builds feedback model with delay approximation policy. | -| `WithPadeOrder` | `mutates` | Option closure configures `SafeFeedback` approximation order. | -| `WithThiranOrder` | `mutates` | Option closure configures `SafeFeedback` fractional-delay policy. | +| `WithPadeOrder` | `mutates` | Option closure configures `Feedback` delay approximation order. | +| `WithThiranOrder` | `mutates` | Option closure configures `Feedback` fractional-delay policy. | | `Loopsens` | `pure`, `returns-mutable` | Returns four new loop-sensitivity models. | | `SS2SS` | `view-in`, `returns-mutable` | Uses transform matrix and returns transformed copy. | | `Xperm` | `view-in`, `returns-mutable` | Returns permuted copy. | @@ -396,7 +395,7 @@ ownership clarity, and release readiness, not shrinking the toolbox shape. | Model containers | `ModelArray`, `GeneralizedModel`, `GeneralizedClosedLoop`, `TunableReal`, `TunableGain`, `TunablePID`, `TunableTF`, `TunableSS` | Mostly private fields with mutating methods; `FreeParameters` exposes parameter pointers. | | Options/workspaces | `C2DOptions`, `TransferFuncOpts`, `StateSpaceOpts`, `FreqRespEstOpts`, `StepInfoOptions`, `SimulateOpts`, `RiccatiOpts`, `RiccatiWorkspace`, `LyapunovOpts`, `LyapunovWorkspace`, `PidtuneOptions`, `SystuneOptions`, `PassivityOptions`, `ReduceOpts`, `ModalTruncateOptions` | Options are caller-owned; workspaces and simulation buffers are mutable and should not be shared concurrently. | | Result structs | `BalrealResult`, `CanonResult`, `GramResult`, `H2SynResult`, `HinfSynResult`, `LqgResult`, `LoopsensResult`, `MarginResult`, `AllMarginResult`, `DiskMarginResult`, `ReduceResult`, `ModalReductionResult`, `ModsepResult`, `PrescaleResult`, `PzmapResult`, `TimeResponse`, `StepInfoResult`, `RiccatiResult`, `RootLocusResult`, `Response`, `SsbalResult`, `StabsepResult`, `StaircaseResult`, `StateSpaceResult`, `TransferFuncResult`, `SystuneResult`, `TuningGoalResult`, `ZerosResult`, `ZPKResult`, `ERAResult`, `FRDPeakGainResult`, `FreqRespEstResult`, `ModelArrayFreqResponse`, `ModelArrayTimeResponse` | Results are mutable data containers; callers should treat them as owned outputs unless workspace-backed docs say otherwise. | -| Value and enum types | `BalredMethod`, `CanonForm`, `AbsorbScope`, `GramType`, `PhysicalPortKind`, `PIDForm`, `PidtuneType`, `C2DMethod`, `C2DDelayModeling`, `FreqRespEstMethod`, `ReduceMode`, `TuningGoalType`, `TuningGoalSpec`, `TuningGoal`, `TunableBounds`, `AnalysisPointLocation`, `AnalysisPoint`, `PhysicalPort`, `PhysicalConnection`, `Connection`, `DampInfo`, `StepMetric`, `NonlinearModel`, `EKFModel`, `NumericBlock`, `TunableBlock`, `FRDResponseMapper`, `PIDOption`, `SafeFeedbackOption` | Mostly value types; callback and option function types may retain references through user code. | +| Value and enum types | `BalredMethod`, `CanonForm`, `AbsorbScope`, `GramType`, `PhysicalPortKind`, `PIDForm`, `PidtuneType`, `C2DMethod`, `C2DDelayModeling`, `FreqRespEstMethod`, `ReduceMode`, `TuningGoalType`, `TuningGoalSpec`, `TuningGoal`, `TunableBounds`, `AnalysisPointLocation`, `AnalysisPoint`, `PhysicalPort`, `PhysicalConnection`, `Connection`, `DampInfo`, `StepMetric`, `NonlinearModel`, `EKFModel`, `NumericBlock`, `TunableBlock`, `FRDResponseMapper`, `PIDOption`, `FeedbackOption` | Mostly value types; callback and option function types may retain references through user code. | ## Release Gates diff --git a/docs/codebase-interface-diagram.md b/docs/codebase-interface-diagram.md index 3c38957..3340723 100644 --- a/docs/codebase-interface-diagram.md +++ b/docs/codebase-interface-diagram.md @@ -39,7 +39,7 @@ flowchart LR subgraph interconnection["Interconnection interfaces"] seriesOp["Series"] parallelOp["Parallel"] - feedbackOp["Feedback / SafeFeedback"] + feedbackOp["Feedback"] connectOps["Append / BlkDiag / Connect
ConnectByName / LFT / SumBlk"] frdConnectOps["FRDSeries / FRDParallel
FRDFeedback / FRDConcat"] delayOps["PadeDelay / ThiranDelay
PullDelaysToLFT / AbsorbDelay
SetDelayModel / GetDelayModel"] diff --git a/docs/codebase-public-interface-map.svg b/docs/codebase-public-interface-map.svg index 8425b4b..6fdcc11 100644 --- a/docs/codebase-public-interface-map.svg +++ b/docs/codebase-public-interface-map.svg @@ -1 +1 @@ -

Design and synthesis

Transformation and reduction

Analysis interfaces

Representation and domain conversion

Interconnection interfaces

Construction and identification

Model interfaces

External callers

System
fundamental state-space model
A, B, C, D, optional E, delays, names

TransferFunc
polynomial-ratio model

ZPK
zero-pole-gain model

FRD
frequency-response data model

ModelArray
compatible model grid

GeneralizedModel / GeneralizedClosedLoop
analysis-point model interface

TunableBlock
tunable gain, PID, TF, or SS block

FreqResponseMatrix
sampled complex response

TimeResponse
sampled time-domain output

New / NewGain / NewFromSlices
NewWithDelay / Rss / Drss

NewDescriptor / ToExplicit

TransferFunc.StateSpace

NewZPK / NewZPKMIMO

NewFRD

NewModelArray / StackModelArrays
ConcatModelArrays

NewGeneralizedModel
NewGeneralizedClosedLoop

ERA
Markov parameters to state-space model

FreqRespEst
sampled input/output to response estimate

Linearize / NewEKF
local nonlinear approximation

NewPhysicalComponent / AssemblePhysical
constraint-based descriptor assembly

Series

Parallel

Feedback / SafeFeedback

Append / BlkDiag / Connect
ConnectByName / LFT / SumBlk

FRDSeries / FRDParallel
FRDFeedback / FRDConcat

PadeDelay / ThiranDelay
PullDelaysToLFT / AbsorbDelay
SetDelayModel / GetDelayModel

System.TransferFunction

System.ZPKModel

System.FRD

System.Discretize / DiscretizeWithOpts
DiscretizeZOH / FOH / Impulse / Matched / D2D

System.Undiscretize / System.D2C

StateTransform / EliminateStates
FixedInputReduction / SelectByName / SelectByIndex

Step / Impulse / Initial / Lsim
Simulate / StepInfo / StepInfoForSystem

GenSig
test-signal generation

FreqResponse / Bode / Nyquist / Nichols
Margin / AllMargin / DiskMargin / Bandwidth / Sigma

Poles / Zeros / Damp / IsStable
DCGain / Pzmap

Gram / HSV / Norm
H2Norm / HinfNorm / Covar

Ctrb / Obsv / IsStabilizable / IsDetectable

Loopsens / RootLocus / FRDMargin

SampledPassive / FRDPassive
Passive compatibility alias / SpectralFactor

SS2SS / Xperm / Canon

Balreal / Balred / Modred / Sminreal
Reduce / MinimalRealization / ModalTruncate

Stabsep / Modsep / Prescale / Ssbal

Inv / Augstate

Care / Dare / Lyap / DLyap

Lqr / Dlqr / Lqi / Lqrd
Place / Acker

Kalman / Kalmd / Lqe / Lqg
Estim / Reg

H2Syn / HinfSyn

NewPID / NewPIDStd / Pidtune
PID / PID2 / SmithPredictor

GridTune / Systune / Looptune
point-specific tuning goals

\ No newline at end of file +

Design and synthesis

Transformation and reduction

Analysis interfaces

Representation and domain conversion

Interconnection interfaces

Construction and identification

Model interfaces

External callers

System
fundamental state-space model
A, B, C, D, optional E, delays, names

TransferFunc
polynomial-ratio model

ZPK
zero-pole-gain model

FRD
frequency-response data model

ModelArray
compatible model grid

GeneralizedModel / GeneralizedClosedLoop
analysis-point model interface

TunableBlock
tunable gain, PID, TF, or SS block

FreqResponseMatrix
sampled complex response

TimeResponse
sampled time-domain output

New / NewGain / NewFromSlices
NewWithDelay / Rss / Drss

NewDescriptor / ToExplicit

TransferFunc.StateSpace

NewZPK / NewZPKMIMO

NewFRD

NewModelArray / StackModelArrays
ConcatModelArrays

NewGeneralizedModel
NewGeneralizedClosedLoop

ERA
Markov parameters to state-space model

FreqRespEst
sampled input/output to response estimate

Linearize / NewEKF
local nonlinear approximation

NewPhysicalComponent / AssemblePhysical
constraint-based descriptor assembly

Series

Parallel

Feedback

Append / BlkDiag / Connect
ConnectByName / LFT / SumBlk

FRDSeries / FRDParallel
FRDFeedback / FRDConcat

PadeDelay / ThiranDelay
PullDelaysToLFT / AbsorbDelay
SetDelayModel / GetDelayModel

System.TransferFunction

System.ZPKModel

System.FRD

System.Discretize / DiscretizeWithOpts
DiscretizeZOH / FOH / Impulse / Matched / D2D

System.Undiscretize / System.D2C

StateTransform / EliminateStates
FixedInputReduction / SelectByName / SelectByIndex

Step / Impulse / Initial / Lsim
Simulate / StepInfo / StepInfoForSystem

GenSig
test-signal generation

FreqResponse / Bode / Nyquist / Nichols
Margin / AllMargin / DiskMargin / Bandwidth / Sigma

Poles / Zeros / Damp / IsStable
DCGain / Pzmap

Gram / HSV / Norm
H2Norm / HinfNorm / Covar

Ctrb / Obsv / IsStabilizable / IsDetectable

Loopsens / RootLocus / FRDMargin

SampledPassive / FRDPassive
Passive compatibility alias / SpectralFactor

SS2SS / Xperm / Canon

Balreal / Balred / Modred / Sminreal
Reduce / MinimalRealization / ModalTruncate

Stabsep / Modsep / Prescale / Ssbal

Inv / Augstate

Care / Dare / Lyap / DLyap

Lqr / Dlqr / Lqi / Lqrd
Place / Acker

Kalman / Kalmd / Lqe / Lqg
Estim / Reg

H2Syn / HinfSyn

NewPID / NewPIDStd / Pidtune
PID / PID2 / SmithPredictor

GridTune / Systune / Looptune
point-specific tuning goals

\ No newline at end of file diff --git a/safe_feedback.go b/feedback_delay.go similarity index 50% rename from safe_feedback.go rename to feedback_delay.go index 26a5bb1..6197602 100644 --- a/safe_feedback.go +++ b/feedback_delay.go @@ -6,39 +6,55 @@ import ( "gonum.org/v1/gonum/mat" ) -type SafeFeedbackOption func(*safeFeedbackConfig) +// FeedbackOption configures how Feedback treats delays when closing the loop. +type FeedbackOption func(*feedbackConfig) -type safeFeedbackConfig struct { - padeOrder int - thiranOrder int +type feedbackConfig struct { + approximateDelays bool + padeOrder int + thiranOrder int } -func WithPadeOrder(n int) SafeFeedbackOption { - return func(c *safeFeedbackConfig) { - c.padeOrder = n +func newFeedbackConfig(opts []FeedbackOption) feedbackConfig { + cfg := feedbackConfig{padeOrder: 5} + for _, o := range opts { + o(&cfg) } + return cfg } -// WithThiranOrder enables Thiran allpass modeling for fractional discrete -// delays in SafeFeedback. Exact integer discrete delays remain state-space -// delays; continuous-time delays use Pade approximation. -func WithThiranOrder(n int) SafeFeedbackOption { - return func(c *safeFeedbackConfig) { - c.thiranOrder = n +// WithApproximatedDelays makes Feedback return a delay-free rational model +// instead of an exact model with internal delays. Continuous-time delays are +// replaced by Pade approximations (default order 5); exact integer discrete +// delays are absorbed into states; fractional discrete delays require +// WithThiranOrder. +func WithApproximatedDelays() FeedbackOption { + return func(c *feedbackConfig) { + c.approximateDelays = true } } -func SafeFeedback(plant, controller *System, sign float64, opts ...SafeFeedbackOption) (*System, error) { - if err := domainMatch(plant, controller); err != nil { - return nil, err +// WithPadeOrder implies WithApproximatedDelays and sets the Pade order used +// for continuous-time delays. +func WithPadeOrder(n int) FeedbackOption { + return func(c *feedbackConfig) { + c.approximateDelays = true + c.padeOrder = n } +} - cfg := safeFeedbackConfig{padeOrder: 5} - for _, o := range opts { - o(&cfg) +// WithThiranOrder implies WithApproximatedDelays and enables Thiran allpass +// modeling for fractional discrete delays. Exact integer discrete delays +// remain state-space delays; continuous-time delays use Pade approximation. +func WithThiranOrder(n int) FeedbackOption { + return func(c *feedbackConfig) { + c.approximateDelays = true + c.thiranOrder = n } +} - strategy := safeFeedbackDelayStrategy{cfg: cfg} +func feedbackWithApproximatedDelays(plant, controller *System, sign float64, cfg feedbackConfig) (*System, error) { + strategy := feedbackDelayStrategy{cfg: cfg} p, c, err := strategy.prepare(plant, controller, sign) if err != nil { return nil, err @@ -53,11 +69,11 @@ func SafeFeedback(plant, controller *System, sign float64, opts ...SafeFeedbackO return result, nil } -type safeFeedbackDelayStrategy struct { - cfg safeFeedbackConfig +type feedbackDelayStrategy struct { + cfg feedbackConfig } -func (s safeFeedbackDelayStrategy) prepare(plant, controller *System, sign float64) (*System, *System, error) { +func (s feedbackDelayStrategy) prepare(plant, controller *System, sign float64) (*System, *System, error) { if plant.IsDiscrete() { p, err := s.replaceDiscreteDelays(plant, "plant") if err != nil { @@ -72,11 +88,11 @@ func (s safeFeedbackDelayStrategy) prepare(plant, controller *System, sign float p, err := replaceContinuousDelays(plant, s.cfg.padeOrder) if err != nil { - return nil, nil, fmt.Errorf("SafeFeedback: pade plant: %w", err) + return nil, nil, fmt.Errorf("Feedback: pade plant: %w", err) } c, err := replaceContinuousDelays(controller, s.cfg.padeOrder) if err != nil { - return nil, nil, fmt.Errorf("SafeFeedback: pade controller: %w", err) + return nil, nil, fmt.Errorf("Feedback: pade controller: %w", err) } if err := s.requireWellPosedPade(p, c, sign); err != nil { return nil, nil, err @@ -84,42 +100,42 @@ func (s safeFeedbackDelayStrategy) prepare(plant, controller *System, sign float return p, c, nil } -func (s safeFeedbackDelayStrategy) replaceDiscreteDelays(sys *System, role string) (*System, error) { +func (s feedbackDelayStrategy) replaceDiscreteDelays(sys *System, role string) (*System, error) { if s.cfg.thiranOrder == 0 { if err := sys.Validate(); err != nil { - return nil, fmt.Errorf("SafeFeedback: validate %s: %w", role, err) + return nil, fmt.Errorf("Feedback: validate %s: %w", role, err) } out, err := sys.AbsorbDelay() if err != nil { - return nil, fmt.Errorf("SafeFeedback: absorb %s: %w", role, err) + return nil, fmt.Errorf("Feedback: absorb %s: %w", role, err) } return out, nil } out, err := replaceDiscreteExternalDelaysWithThiran(sys, s.cfg.thiranOrder) if err != nil { - return nil, fmt.Errorf("SafeFeedback: thiran %s: %w", role, err) + return nil, fmt.Errorf("Feedback: thiran %s: %w", role, err) } return out, nil } -func (s safeFeedbackDelayStrategy) requireWellPosedPade(plant, controller *System, sign float64) error { +func (s feedbackDelayStrategy) requireWellPosedPade(plant, controller *System, sign float64) error { _, mPlant, pPlant := plant.Dims() _, mCtrl, pCtrl := controller.Dims() if pPlant != mCtrl || pCtrl != mPlant { return nil } - if _, err := solveFeedbackFeedthrough(plant.D, controller.D, sign, pPlant, "SafeFeedback", ErrAlgebraicLoop); err != nil { - return fmt.Errorf("SafeFeedback: Pade approximation creates singular algebraic loop; try a different padeOrder (even vs odd) to flip feedthrough sign") + if _, err := solveFeedbackFeedthrough(plant.D, controller.D, sign, pPlant, "Feedback", ErrAlgebraicLoop); err != nil { + return fmt.Errorf("Feedback: Pade approximation creates singular algebraic loop; try a different padeOrder (even vs odd) to flip feedthrough sign") } return nil } func replaceDiscreteExternalDelaysWithThiran(sys *System, thiranOrder int) (*System, error) { - return newDelayConversionPolicy(sys.Dt, thiranOrder, 0).replaceDiscreteExternal(sys, "SafeFeedback") + return newDelayConversionPolicy(sys.Dt, thiranOrder, 0).replaceDiscreteExternal(sys, "Feedback") } func replaceContinuousDelays(sys *System, padeOrder int) (*System, error) { - return newDelayConversionPolicy(sys.Dt, 0, padeOrder).replaceContinuousExternal(sys, "SafeFeedback") + return newDelayConversionPolicy(sys.Dt, 0, padeOrder).replaceContinuousExternal(sys, "Feedback") } func buildDiagWithPade(channel, size int, pade *System) (*System, error) { diff --git a/testmatlab_delay_test.go b/testmatlab_delay_test.go index 84044d8..207eac5 100644 --- a/testmatlab_delay_test.go +++ b/testmatlab_delay_test.go @@ -414,7 +414,7 @@ func TestFeedback_MATLAB_PID_DeadTime(t *testing.T) { t.Fatal(err) } - T, err := SafeFeedback(P, C, -1, WithPadeOrder(5)) + T, err := Feedback(P, C, -1, WithPadeOrder(5)) if err != nil { t.Fatal(err) }