feat: WP5 조성 trim rate 제한 추가 (CompRate, 듀티 대역폭 placeholder)

trim을 clamp(±5%) 후 RateLimiter로 완만히 이동(rate≤1.0/min placeholder, 현장 calibrate). 과도/역전 시 trim→0 graceful. 49/49.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
This commit is contained in:
windpacer
2026-06-01 21:22:54 +09:00
parent 49cf04569e
commit 02ada31e3c

View File

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