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 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<StreamAdvisory> 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 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(CB)"; }
else if (a.Key.Equals("D", StringComparison.OrdinalIgnoreCase)) { dev = -devUp; src = "상부front(CD)"; }
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();
}