diff --git a/src/Infrastructure/Control/FeedforwardEngine.cs b/src/Infrastructure/Control/FeedforwardEngine.cs index 5eebecc..cb0aa91 100644 --- a/src/Infrastructure/Control/FeedforwardEngine.cs +++ b/src/Infrastructure/Control/FeedforwardEngine.cs @@ -7,6 +7,7 @@ public sealed class StreamState public DeadTimeBuffer Dead { get; } = new(); public FirstOrderLag Lag { get; } = new(); public RateLimiter Rate { get; } = new(); + public RateLimiter CompRate { get; } = new(); // WP5: 조성 trim rate 제한(듀티 대역폭) public double LastRec { get; set; } = double.NaN; } @@ -138,7 +139,7 @@ public sealed class FeedforwardEngine var (frontState, frontTrim) = ApplyFront(cfg, st, ts, temps, transient); // WO-5 프론트 위치 var tp = JudgeTempProfile(temps, st, transient); // §10 역전 판정 var f2 = ApplyFront2Point(st, ts, temps, transient); // WP5 1단계 2-point front - ApplyCompositionTrim(cfg, ff, st, f2, transient, tp.State, ref outs); // WP5 2·3단계 조성base+trim + ApplyCompositionTrim(cfg, ff, ts, st, f2, transient, tp.State, ref outs); // WP5 2·3단계 조성base+trim // 역전/붕괴 시 front 트림 보류(방향 신뢰 불가) if (tp.State is "온도역전" or "프로파일붕괴") frontTrim = null; var (mode, modeReason, feedRecSp) = ApplyRecovery( @@ -340,13 +341,14 @@ public sealed class FeedforwardEngine // ── WP5 2·3단계: 조성목표 base(분율×feed) + 유계 trim(±5% clamp). LevelDriven 드로우 advisory(쓰기 없음). ── // 분율 = sc.TargetCoeff(수동입력 시 supervisor가 치환). trim 게인·부호는 현장 calibrate 필요(데모 검증 불가). - private static void ApplyCompositionTrim(ColumnConfig cfg, double ff, ColumnState st, + private static void ApplyCompositionTrim(ColumnConfig cfg, double ff, double ts, ColumnState st, (string? upState, double? upMetric, string? loState, double? loMetric) f2, bool transient, string? tempProfileState, ref List outs) { - const double clampFrac = 0.05; // ±5%(보수, 결정) - const double gainPerDeg = 0.01; // 1%/°C placeholder — 현장 calibrate - bool block = transient || tempProfileState is "온도역전" or "프로파일붕괴"; // 과도/역전 시 trim 0 + const double clampFrac = 0.05; // ±5%(보수, 결정) + const double gainPerDeg = 0.01; // 1%/°C placeholder — 현장 calibrate + const double trimRatePerMin = 1.0; // trim rate 제한[/min] placeholder — 듀티 대역폭 현장 calibrate + bool block = transient || tempProfileState is "온도역전" or "프로파일붕괴"; // 과도/역전 시 trim→0 double devUp = f2.upMetric.HasValue ? f2.upMetric.Value - st.UpperFrontBase.Value : double.NaN; double devLo = f2.loMetric.HasValue ? f2.loMetric.Value - st.LowerFrontBase.Value : double.NaN; @@ -356,21 +358,20 @@ public sealed class FeedforwardEngine var sc = cfg.Streams.FirstOrDefault(x => x.Key == a.Key); if (sc is null || ff <= 1e-6) return a; double baseSp = sc.TargetCoeff * ff; // 분율×feed + var stt = st.Stream(a.Key); double dev; string src; if (a.Key.Equals("B", StringComparison.OrdinalIgnoreCase)) { dev = devLo; src = "하부front(C−B)"; } else if (a.Key.Equals("D", StringComparison.OrdinalIgnoreCase)) { dev = -devUp; src = "상부front(C−D)"; } else return a with { CompositionBase = baseSp, RecommendedSpComposition = baseSp, TrimSource = "trim 미적용" }; - if (block || !Num.IsFinite(dev)) - return a with { CompositionBase = baseSp, Trim = 0.0, RecommendedSpComposition = baseSp, - TrimSource = $"{src} (과도/역전/무신호 — trim 0)" }; - - double trim = Num.Clamp(gainPerDeg * dev, -clampFrac, clampFrac) * baseSp; - return a with { - CompositionBase = baseSp, Trim = trim, RecommendedSpComposition = baseSp + trim, - TrimSource = $"{src} dev={dev:F2}°C (±{clampFrac:P0} clamp · 게인·부호 현장 calibrate · rate제한 후속)" - }; + // 목표 trim(clamp). 과도/역전/무신호면 0으로. 그 다음 rate 제한으로 완만히 이동. + double rawTrim = (block || !Num.IsFinite(dev)) ? 0.0 : Num.Clamp(gainPerDeg * dev, -clampFrac, clampFrac) * baseSp; + double trim = stt.CompRate.Step(rawTrim, trimRatePerMin, trimRatePerMin, ts); + string note = (block || !Num.IsFinite(dev)) + ? $"{src} (과도/역전/무신호 — trim→0)" + : $"{src} dev={dev:F2}°C (±{clampFrac:P0} clamp · rate≤{trimRatePerMin}/min · 게인·부호 현장 calibrate)"; + return a with { CompositionBase = baseSp, Trim = trim, RecommendedSpComposition = baseSp + trim, TrimSource = note }; }).ToList(); }