From 48a6b6be574a592526ec44a1dce1ccab2dd6479f Mon Sep 17 00:00:00 2001 From: windpacer Date: Sun, 31 May 2026 17:31:47 +0900 Subject: [PATCH] =?UTF-8?q?docs:=20=EC=B8=A1=EB=A5=98=EC=B6=94=EC=B6=9C=20?= =?UTF-8?q?=ED=86=B5=ED=95=A9=EC=9C=A0=EB=9F=89=EC=84=A4=EC=A0=95=EA=B3=B5?= =?UTF-8?q?=EC=8B=9D=20=EC=84=A4=EA=B3=84=EB=AC=B8=EC=84=9C=20(Phase=20I/I?= =?UTF-8?q?I/III=20+=20=EB=B6=84=EC=84=9D=EC=97=94=EC=A7=84)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- ...ฅ˜์ถ”์ถœ์‹-ํ†ตํ•ฉ์œ ๋Ÿ‰์„ค์ •๊ณต์‹-๊ตฌํ˜„์ฝ”๋”ฉ-PhaseI.md | 1067 +++++++++++++++ ...๋Ÿ‰์„ค์ •๊ณต์‹-๊ตฌํ˜„์ฝ”๋”ฉ-PhaseII-๋ถ„์„์—”์ง„+์ „ํ™˜๋ฅ˜๋ณต๊ท€.md | 439 +++++++ ...˜์ถ”์ถœ์‹-ํ†ตํ•ฉ์œ ๋Ÿ‰์„ค์ •๊ณต์‹-๊ตฌํ˜„์ฝ”๋”ฉ-PhaseII.md | 514 ++++++++ ...์ถ”์ถœ์‹-ํ†ตํ•ฉ์œ ๋Ÿ‰์„ค์ •๊ณต์‹-๊ตฌํ˜„์ฝ”๋”ฉ-PhaseIII.md | 337 +++++ docs/์ธก๋ฅ˜์ถ”์ถœ์‹-ํ†ตํ•ฉ์œ ๋Ÿ‰์„ค์ •๊ณต์‹.md | 1168 +++++++++++++++++ 5 files changed, 3525 insertions(+) create mode 100644 docs/์ธก๋ฅ˜์ถ”์ถœ์‹-ํ†ตํ•ฉ์œ ๋Ÿ‰์„ค์ •๊ณต์‹-๊ตฌํ˜„์ฝ”๋”ฉ-PhaseI.md create mode 100644 docs/์ธก๋ฅ˜์ถ”์ถœ์‹-ํ†ตํ•ฉ์œ ๋Ÿ‰์„ค์ •๊ณต์‹-๊ตฌํ˜„์ฝ”๋”ฉ-PhaseII-๋ถ„์„์—”์ง„+์ „ํ™˜๋ฅ˜๋ณต๊ท€.md create mode 100644 docs/์ธก๋ฅ˜์ถ”์ถœ์‹-ํ†ตํ•ฉ์œ ๋Ÿ‰์„ค์ •๊ณต์‹-๊ตฌํ˜„์ฝ”๋”ฉ-PhaseII.md create mode 100644 docs/์ธก๋ฅ˜์ถ”์ถœ์‹-ํ†ตํ•ฉ์œ ๋Ÿ‰์„ค์ •๊ณต์‹-๊ตฌํ˜„์ฝ”๋”ฉ-PhaseIII.md create mode 100644 docs/์ธก๋ฅ˜์ถ”์ถœ์‹-ํ†ตํ•ฉ์œ ๋Ÿ‰์„ค์ •๊ณต์‹.md diff --git a/docs/์ธก๋ฅ˜์ถ”์ถœ์‹-ํ†ตํ•ฉ์œ ๋Ÿ‰์„ค์ •๊ณต์‹-๊ตฌํ˜„์ฝ”๋”ฉ-PhaseI.md b/docs/์ธก๋ฅ˜์ถ”์ถœ์‹-ํ†ตํ•ฉ์œ ๋Ÿ‰์„ค์ •๊ณต์‹-๊ตฌํ˜„์ฝ”๋”ฉ-PhaseI.md new file mode 100644 index 0000000..4ddd833 --- /dev/null +++ b/docs/์ธก๋ฅ˜์ถ”์ถœ์‹-ํ†ตํ•ฉ์œ ๋Ÿ‰์„ค์ •๊ณต์‹-๊ตฌํ˜„์ฝ”๋”ฉ-PhaseI.md @@ -0,0 +1,1067 @@ +# ์ธก๋ฅ˜์ถ”์ถœ ํ†ตํ•ฉ์œ ๋Ÿ‰ โ€” ยง12 ์—”์ง„ ๊ตฌํ˜„ ์ฝ”๋”ฉ (Phase I) + +> **๋ณธ ๋ฌธ์„œ์˜ ์„ฑ๊ฒฉ**: `์ธก๋ฅ˜์ถ”์ถœ์‹-ํ†ตํ•ฉ์œ ๋Ÿ‰์„ค์ •๊ณต์‹.md` ยง12(advisory ์—”์ง„)์˜ **๊ตฌํ˜„ ์ฝ”๋”ฉ ๋ช…์„ธ + ๊ฒ€์ฆ ์ ˆ์ฐจ**. +> **๊ฐ๋…์ž(auditor)๊ฐ€ `diagnosis-checklist.md` 8๋‹จ๊ณ„๋กœ ์ง„๋‹จํ•œ ๋’ค ์‹ค์ œ ํ”„๋กœ์ ํŠธ์— ๋ฐ˜์˜**ํ•œ๋‹ค. +> **์ง„๋‹จ ํ†ต๊ณผ ์ „์—๋Š” DI ๋“ฑ๋กยท๋นŒ๋“œ ํŽธ์ž… ๊ธˆ์ง€** (์ฝ”๋“œ๋Š” ์‹ ๊ทœ ํŒŒ์ผ๋กœ๋งŒ ์กด์žฌ, ๊ธฐ์กด ๋™์ž‘ ๋ฌด์˜ํ–ฅ). + +**์ „์ œ(๋ถˆ๋ณ€์‹)**: ๋ณธ Phase๋Š” **advisory(๋ณด์กฐ์ง€ํ‘œ) ์ „์šฉ** โ€” ์ œ์–ด ๋ ˆ์ง€์Šคํ„ฐ(SP/OP)์— **์“ฐ๊ธฐ ํ˜ธ์ถœ 0๊ฑด**. +AUTO/MANUAL **๋ฌด๊ด€**ํ•˜๊ฒŒ ๊ถŒ์žฅ SP๋ฅผ ๊ณ„์‚ฐํ•ด ์ €์žฅยทํ‘œ์‹œ๋งŒ ํ•œ๋‹ค. RSP ์“ฐ๊ธฐ๋Š” Phase III. + +--- + +## 0. Phase ๋ถ„ํ•  (๋ฌด์—‡์„ ์ง€๊ธˆ ์ฝ”๋”ฉํ•˜๋‚˜) + +| ๋ฒ”์œ„ | Phase I (๋ณธ ๋ฌธ์„œ, ์ฝ”๋“œ) | Phase II (ํ”Œ๋žœ) | Phase III (ํ”Œ๋žœ) | +|:-----|:----|:----|:----| +| ์ˆœ์ˆ˜ ์—ฐ์‚ฐ๋ธ”๋ก | EMA/MAยทDeadTimeBufferยทFirstOrderLagยทRateLimiter(๋น„๋Œ€์นญ)ยทClampยทDerivativeยทPressureComp | CrossCorrLagEstimatorยทDiffTempยทFrontPositionIndicator | โ€” | +| ์—”์ง„ | `FeedforwardEngine.Tick`(role-aware, ๊ณผ๋„๊ฒŒ์ดํŠธ, confidence) | ฮธ ์ž๋™ํŠœ๋‹ยท๋А๋ฆฐ ๋ฐ”์ด์–ด์Šค ์ ์‘ยทanalyzer corroborate | โ€” | +| ์ˆ˜๊ธ‰ | realtime_table PV ์ฝ๊ธฐ + AdvisoryStore | โ€” | RSP ์“ฐ๊ธฐ + WriteGuard + ์›Œ์น˜๋…ยท๋ฐ๋“œ๋งจ | +| ์„ค์ • | DB ํ…Œ์ด๋ธ” + ๋กœ๋” | Web UI ์„ค์ •/๋Œ€์‹œ๋ณด๋“œ(Tab 18) | ์šด์ „์› ์ฑ„ํƒ ์Šค์œ„์น˜ | +| ์ถœ๋ ฅ | ๊ถŒ์žฅ SP ์ €์žฅ + ์ฝ๊ธฐ API | ํ™”๋ฉด ์‹œ๊ฐํ™” | ์ปจํŠธ๋กค๋Ÿฌ SP ์ถ”์ข… | + +ยง13(์˜จ๋„๊ธฐ๋ฐ˜ ฮธ/sweet-spot)ยทยง14(์˜ค์ฐจ์˜ˆ์‚ฐ ๋“ฑ๊ธ‰ยท๊ณ„์ ˆ ๋ฐ”์ด์–ด์Šค)์˜ **๋ณด์ • ํ•ญ๋ชฉ์€ ยง6 ํ”Œ๋žœ**์— ๋งคํ•‘. + +--- + +## 1. ํŒŒ์ผ ๋ฐฐ์น˜ & ๋„ค์ž„์ŠคํŽ˜์ด์Šค (์‹ ๊ทœ๋งŒ) + +``` +src/Core/Application/Feedforward/ + FeedforwardModels.cs # enumยทColumnConfigยทStreamConfigยทPvSnapshotยทAdvisoryResult (record) + IFeedforwardStores.cs # IFeedforwardConfigStoreยทIFeedforwardAdvisoryStore +src/Infrastructure/Control/ + ComputationBlocks.cs # ์ˆœ์ˆ˜ ์—ฐ์‚ฐ๋ธ”๋ก (๋‹จ์œ„ํ…Œ์ŠคํŠธ ๋Œ€์ƒ) + FeedforwardEngine.cs # Tick (์ˆœ์ˆ˜ ํ•จ์ˆ˜, I/O ์—†์Œ) + FeedforwardSupervisor.cs # BackgroundService โ€” PV ์ฝ๊ธฐโ†’Tickโ†’์ €์žฅ (์“ฐ๊ธฐ ์—†์Œ) + FeedforwardAdvisoryStore.cs # in-memory ์ตœ์‹  ๊ฒฐ๊ณผ + FeedforwardConfigStore.cs # DB ๋กœ๋” (ff_column_config / ff_stream_config) +src/Web/Controllers/ + FeedforwardController.cs # GET advisory/config (camelCase), admin config CRUD +tests/ (๋˜๋Š” ์‹ ๊ทœ ํ…Œ์ŠคํŠธ ํ”„๋กœ์ ํŠธ) + FeedforwardBlocksTests.cs / FeedforwardEngineTests.cs +``` + +๋„ค์ž„์ŠคํŽ˜์ด์Šค: `ExperionCrawler.Core.Application.Feedforward`, `ExperionCrawler.Infrastructure.Control`, +์ปจํŠธ๋กค๋Ÿฌ๋Š” `ExperionCrawler.Web.Controllers`. + +### 1.1 ํ”„๋กœ์ ํŠธ ๊ตฌ์กฐ ์ „์ œ (G1) โ˜… + +- **๋‹จ์ผ ํ”„๋กœ์ ํŠธ**: ์•ฑ ์ฝ”๋“œ๋Š” ์ „๋ถ€ **`src/Web/ExperionCrawler.csproj`**(`Microsoft.NET.Sdk.Web`) ํ•˜๋‚˜๋กœ ์ปดํŒŒ์ผ๋œ๋‹ค. + `src/Core`ยท`src/Infrastructure`ยท`src/Web`๋Š” **ํด๋”์ผ ๋ฟ ๋ณ„๋„ csproj๊ฐ€ ์•„๋‹ˆ๋‹ค.** +- ๋”ฐ๋ผ์„œ ์œ„ ์‹ ๊ทœ ํŒŒ์ผ๋“ค์€ **์ƒˆ csprojยทProjectReference๋ฅผ ๋งŒ๋“ค์ง€ ๋ง๊ณ ** ํ•ด๋‹น ํด๋”์— ์ถ”๊ฐ€๋งŒ ํ•˜๋ฉด ๊ฐ™์€ ํ”„๋กœ์ ํŠธ๋กœ ๋นŒ๋“œ๋œ๋‹ค. +- ๋นŒ๋“œ: `dotnet build src/Web/ExperionCrawler.csproj` (์†”๋ฃจ์…˜ ํŒŒ์ผ ์—†์Œ). +- ๋‹จ, **ํ…Œ์ŠคํŠธ ํ”„๋กœ์ ํŠธ๋งŒ** ๋ณ„๋„ csproj๋กœ ์‹ ์„คํ•œ๋‹ค(ยง5.7). + +--- + +## 2. ์ฝ”๋“œ โ€” Core ๋ชจ๋ธ (`FeedforwardModels.cs`) + +```csharp +namespace ExperionCrawler.Core.Application.Feedforward; + +/// ์ŠคํŠธ๋ฆผ ์—ญํ• . ยง9.3 D5: ๋ ˆ๋ฒจ ํ๋ฃจํ”„๊ฐ€ ์žˆ์œผ๋ฉด DยทB๋Š” LevelDriven. +public enum StreamRole { Commanded, LevelDriven, Monitor } + +/// ๋ณด์ • ์‹ ๋ขฐ๋„ ๋“ฑ๊ธ‰. ยง14.3 A ๊ฒฌ๊ณ  / B ํ•œ๊ณ„ / C ์ทจ์•ฝ. +public enum Confidence { A, B, C } + +/// ์ŠคํŠธ๋ฆผ(์œ ๋Ÿ‰ 1๊ฐœ) ์„ค์ •. ๊ฒฝํ—˜์ƒ์ˆ˜๋Š” Web UI(Phase II)์—์„œ ๊ณต๊ธ‰, DB ์ €์žฅ. +public sealed record StreamConfig +{ + public string Key { get; init; } = ""; // ๋…ผ๋ฆฌ๋ช…: "D","P","B","R" + public string FlowTag { get; init; } = ""; // realtime base tag: "ficq-6118" + public StreamRole Role { get; init; } = StreamRole.Monitor; + public double TargetCoeff { get; init; } // K_t (commanded/levelDriven), ๋˜๋Š” R_f(reflux) + public double ThetaUpSec { get; init; } // ์ „๋‹ฌ ๋ฐ๋“œํƒ€์ž„(ํ”ผ๋“œ ์ƒ์Šน), D7 ๋น„๋Œ€์นญ + public double ThetaDnSec { get; init; } // ์ „๋‹ฌ ๋ฐ๋“œํƒ€์ž„(ํ”ผ๋“œ ํ•˜๊ฐ•) + public double TauSec { get; init; } // 1์ฐจ ์ง€์ฒด + public double SpMin { get; init; } + public double SpMax { get; init; } = double.MaxValue; + public double RateUpPerMin { get; init; } = double.MaxValue; + public double RateDnPerMin { get; init; } = double.MaxValue; + public bool RefluxFromProduct { get; init; } // R = R_f ร— P_sp + public Confidence Grade { get; init; } = Confidence.A; +} + +/// ์ปฌ๋Ÿผ 1๊ฐœ ์„ค์ •. ๋‹ค์ค‘ ์ปฌ๋Ÿผ ๊ณต์œ  โ€” ์ƒˆ ์ปฌ๋Ÿผ = row ์ถ”๊ฐ€๋งŒ. +public sealed record ColumnConfig +{ + public int Id { get; init; } + public string Name { get; init; } = ""; + public bool Enabled { get; init; } + public bool AdvisoryOnly { get; init; } = true; // Phase I ๊ฐ•์ œ true + public string FeedTag { get; init; } = ""; // base tag (".pv"๋Š” ๋กœ๋”๊ฐ€ ๋ถ€์ฐฉ) + public string? PressureTag { get; init; } + public IReadOnlyList LevelTags { get; init; } = Array.Empty(); + public double ScanSec { get; init; } = 2.0; + public double FeedFilterTauSec { get; init; } = 300.0; // ยง11.5 ๋…ธ์ด์ฆˆ ํ•„ํ„ฐ + public double FeedMoveThresholdPerMin { get; init; } = 0.0; // ๊ณผ๋„ ํŒ์ •(0=๋น„ํ™œ์„ฑ) + public double PressFilterTauSec { get; init; } = 60.0; // ์••๋ ฅ 1์ฐจ์ €์—ญํ†ต๊ณผ ์‹œ์ •์ˆ˜(์ดˆ) โ€” ์›์‹œ์••๋ ฅ ๋Œ€๋น„ ํ•„ํ„ฐ๊ฐ’์ด PressureBand ์ด์ƒ ๋ฒ—์–ด๋‚˜๋ฉด ๊ณผ๋„ํŒ์ • + public double PressureBand { get; init; } = double.MaxValue; + public double SettleSec { get; init; } = 0.0; // T_SETTLE + public double StaleSec { get; init; } = 120.0; // PV ์‹ ์„ ๋„ ํ•œ๊ณ„ + public string? ProductKey { get; init; } = "P"; // reflux ์ฐธ์กฐ ๋Œ€์ƒ + public IReadOnlyList Streams { get; init; } = Array.Empty(); +} + +/// ์ฝ์€ PV 1๊ฐœ (์‹ ์„ ๋„ยทํ’ˆ์งˆ ํฌํ•จ). +public sealed record TagSample(string Tag, double Value, bool Good, DateTime Timestamp); + +/// ํ•œ ์Šค์บ”์˜ ์ž…๋ ฅ ์Šค๋ƒ…์ƒท. +public sealed record PvSnapshot( + TagSample Feed, + TagSample? Pressure, + IReadOnlyList Levels, + IReadOnlyDictionary Streams); // key = StreamConfig.Key + +/// ์ŠคํŠธ๋ฆผ๋ณ„ ๊ถŒ์žฅ ๊ฒฐ๊ณผ. +public sealed record StreamAdvisory( + string Key, string FlowTag, StreamRole Role, + double Pv, double? RecommendedSp, double? Gap, + int Trend, // -1 ํ•˜๊ฐ• / 0 / +1 ์ƒ์Šน + bool Valid, // ๊ณผ๋„ ์ค‘์ด๋ฉด false("์ •์ฐฉ ๋Œ€๊ธฐ") + Confidence Grade, + string Note); + +/// ์ปฌ๋Ÿผ 1ํšŒ Tick ๊ฒฐ๊ณผ (์ €์žฅยทํ‘œ์‹œ ์ „์šฉ). +public sealed record AdvisoryResult( + int ColumnId, string ColumnName, DateTime ComputedAt, + bool Enabled, bool Transient, string TransientReason, + double FeedFiltered, + IReadOnlyList Streams, + double? VLoss, double? Yield, string MassBalanceState); +``` + +--- + +## 3. ์ฝ”๋“œ โ€” ์ˆœ์ˆ˜ ์—ฐ์‚ฐ๋ธ”๋ก (`ComputationBlocks.cs`) + +> ๋ชจ๋‘ **I/O ์—†์Œยท๊ฒฐ์ •๋ก ์ ** โ†’ ๋‹จ์œ„ํ…Œ์ŠคํŠธ ์šฉ์ด. ๊ฐ ์ธ์Šคํ„ด์Šค๋Š” **๋‹จ์ผ ์Šค๋ ˆ๋“œ(์—”์ง„ ๋ฃจํ”„)** ์†Œ์œ  โ†’ ๋ฝ ๋ถˆํ•„์š”. + +```csharp +namespace ExperionCrawler.Infrastructure.Control; + +public static class Num +{ + public static double Clamp(double x, double lo, double hi) => Math.Max(lo, Math.Min(hi, x)); + public static bool IsFinite(double x) => !double.IsNaN(x) && !double.IsInfinity(x); +} + +/// 1์ฐจ ์ €์—ญํ†ต๊ณผ(EMA). DCS Lag/Filter ๋ธ”๋ก ๋“ฑ๊ฐ€. +public sealed class FirstOrderLag +{ + private double _y; + private bool _seeded; + public double Value => _y; + public bool Seeded => _seeded; + public void Seed(double v) { _y = v; _seeded = true; } + public double Step(double x, double tauSec, double tsSec) + { + if (!_seeded) { Seed(x); return _y; } + if (tauSec <= 0.0) { _y = x; return _y; } + var a = tsSec / (tauSec + tsSec); // 0์ง„์งœ ์œˆ๋„์šฐ ์ด๋™ํ‰๊ท (MA). ๋…ธ์ด์ฆˆ ์ œ๊ฑฐ ๋Œ€์•ˆ(ยง11.5). DCS๊ฐ€ ๊ตฌํ˜„ ์–ด๋ ค์šด ๋ธ”๋ก. +public sealed class MovingAverage +{ + private readonly Queue _buf = new(); + private readonly int _window; + private double _sum; + public MovingAverage(int windowSamples) => _window = Math.Max(1, windowSamples); + public double Push(double x) + { + _buf.Enqueue(x); _sum += x; + while (_buf.Count > _window) _sum -= _buf.Dequeue(); + return _sum / _buf.Count; + } +} + +/// ๊ฐ€๋ณ€ ์ „๋‹ฌ์ง€์—ฐ(๋ฐ๋“œํƒ€์ž„) ๋ง๋ฒ„ํผ. ์šฉ๋Ÿ‰์€ ์š”์ฒญ๋œ ์ตœ๋Œ€ n ์œผ๋กœ **์ฆ๊ฐ€๋งŒ**(์ถ•์†Œ ๊ธˆ์ง€), +/// ฮธ(=n)๊ฐ€ ์Šค์บ”๋งˆ๋‹ค ๋ฐ”๋€Œ์–ด๋„(๋น„๋Œ€์นญ ฮธup/ฮธdn, D7) **์ฝ๊ธฐ ์˜คํ”„์…‹๋งŒ ๊ฐ€๋ณ€** โ€” ํžˆ์Šคํ† ๋ฆฌ ๋ณด์กด. +/// โ€ป ์ง„๋‹จ ์ •์ •(2026-05-30): n ๋ณ€๊ฒฝ ์‹œ Resize-์žฌ์‹œ๋“œํ•˜๋˜ ์ดˆ์•ˆ์€ ๋น„๋Œ€์นญ ฮธ์—์„œ ๋ถ€ํ˜ธ๋ฐ˜์ „๋งˆ๋‹ค ์ง€์—ฐ์„  ์†Œ์‹ค โ†’ ํ๊ธฐ. +public sealed class DeadTimeBuffer +{ + private double[] _buf = Array.Empty(); // ํ•ญ์ƒ ๊ฐ€๋“ ์ฐฌ ๋ง(์‹œ๋“œ ์‚ฌ์ „์ถฉ์ „) + private int _cap; // ์šฉ๋Ÿ‰(๋ณด์กด ์ƒ˜ํ”Œ ์ˆ˜). ์ฆ๊ฐ€๋งŒ. + private int _head; // ๋‹ค์Œ ๋ฎ์–ด์“ธ ์œ„์น˜(=๊ฐ€์žฅ ์˜ค๋ž˜๋œ) + private bool _seeded; + + public double Through(double x, double thetaSec, double tsSec) + { + int n = (int)Math.Round(thetaSec / Math.Max(1e-6, tsSec)); + if (n <= 0) return x; // ฮธ๋น„๋Œ€์นญ ๋ณ€ํ™”์œจ ์ œํ•œ(/min). ยง11.4 D7 (upโ‰ dn). +public sealed class RateLimiter +{ + private double _last; + private bool _seeded; + public double Last => _last; + public void Seed(double v) { _last = v; _seeded = true; } + public double Step(double target, double rateUpPerMin, double rateDnPerMin, double tsSec) + { + if (!_seeded) { Seed(target); return _last; } + var up = Math.Abs(rateUpPerMin) * tsSec / 60.0; + var dn = Math.Abs(rateDnPerMin) * tsSec / 60.0; + var d = Num.Clamp(target - _last, -dn, up); + _last += d; + return _last; + } +} + +/// per-second ๋ฏธ๋ถ„(dF/dt, dM/dt). +public sealed class Derivative +{ + private double _prev; + private bool _seeded; + public double Update(double x, double tsSec) + { + if (!_seeded) { _prev = x; _seeded = true; return 0.0; } + var d = (x - _prev) / Math.Max(1e-6, tsSec); + _prev = x; + return d; + } +} + +/// ์••๋ ฅ๋ณด์ •์˜จ๋„ PCT = T - dTdPยท(P-Pref). ยง13.3 (Phase I๋Š” ๋ชจ๋‹ˆํ„ฐ ๋ณด์กฐ). +public static class TempCorrection +{ + public static double PressureCompensated(double tMeas, double p, double pRef, double dTdP) + => tMeas - dTdP * (p - pRef); +} +``` + +--- + +## 4. ์ฝ”๋“œ โ€” ์—”์ง„ (`FeedforwardEngine.cs`) + +> **์ˆœ์ˆ˜ ํ•จ์ˆ˜**: I/Oยท์‹œ๊ฐ„ยทDB ์ ‘๊ทผ ์—†์Œ. ์ž…๋ ฅ=cfgยทsnapshotยทstate, ์ถœ๋ ฅ=AdvisoryResult. +> ์ƒํƒœ(`ColumnState`)๋Š” ํ˜ธ์ถœ์ž(Supervisor)๊ฐ€ ์ปฌ๋Ÿผ๋ณ„ 1๊ฐœ ๋ณด์œ  โ†’ ์ปฌ๋Ÿผ ๊ฐ„ ๊ฒฉ๋ฆฌ. + +```csharp +using ExperionCrawler.Core.Application.Feedforward; + +namespace ExperionCrawler.Infrastructure.Control; + +public sealed class StreamState +{ + public DeadTimeBuffer Dead { get; } = new(); + public FirstOrderLag Lag { get; } = new(); + public RateLimiter Rate { get; } = new(); + public double LastRec { get; set; } = double.NaN; +} + +public sealed class ColumnState +{ + public FirstOrderLag FeedFilter { get; } = new(); + public FirstOrderLag PressFilter { get; } = new(); + public Derivative FeedDeriv { get; } = new(); + public double SettleTimerSec { get; set; } + public bool Initialized { get; set; } + public Dictionary Streams { get; } = new(); + + public StreamState Stream(string key) + { + if (!Streams.TryGetValue(key, out var s)) { s = new StreamState(); Streams[key] = s; } + return s; + } +} + +public sealed class FeedforwardEngine +{ + public AdvisoryResult Tick(ColumnConfig cfg, PvSnapshot pv, ColumnState st, DateTime now) + { + var ts = cfg.ScanSec; + + // โ”€โ”€ 0) FEED ํ’ˆ์งˆ ๊ฒŒ์ดํŠธ โ†’ BAD๋ฉด ์ง์ „ ๊ถŒ์žฅ ํ™€๋“œ โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€ + if (!pv.Feed.Good || !Num.IsFinite(pv.Feed.Value)) + return Hold(cfg, st, now, "FEED BAD"); + + // โ”€โ”€ 1) FEED ๋…ธ์ด์ฆˆ ํ•„ํ„ฐ โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€ + var ff = st.FeedFilter.Step(pv.Feed.Value, cfg.FeedFilterTauSec, ts); + + // ์ตœ์ดˆ ์ •์ƒ๊ฐ’์œผ๋กœ bumpless ์‹œ๋“œ + if (!st.Initialized) { SeedAll(cfg, pv, st, ff); st.Initialized = true; } + + // โ”€โ”€ 2) ๊ณผ๋„/์••๋ ฅ ๊ฒŒ์ดํŠธ (ยง11.6 D6ยทD11) โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€ + var dF = st.FeedDeriv.Update(ff, ts); // per sec + bool moving = cfg.FeedMoveThresholdPerMin > 0 + && Math.Abs(dF) * 60.0 > cfg.FeedMoveThresholdPerMin; + bool pUnstable = false; + if (pv.Pressure is { Good: true } pp && Num.IsFinite(pp.Value)) + { + var pf = st.PressFilter.Step(pp.Value, cfg.PressFilterTauSec, ts); + pUnstable = Math.Abs(pp.Value - pf) > cfg.PressureBand; + } + if (moving || pUnstable) st.SettleTimerSec = cfg.SettleSec; + else st.SettleTimerSec = Math.Max(0.0, st.SettleTimerSec - ts); + bool transient = moving || pUnstable || st.SettleTimerSec > 0.0; + string treason = moving ? "FEED ์ด๋™" + : pUnstable ? "์••๋ ฅ ๋ถˆ์•ˆ์ •" + : st.SettleTimerSec > 0.0 ? $"์ •์ฐฉ ๋Œ€๊ธฐ {st.SettleTimerSec:F0}s" : ""; + + // โ”€โ”€ 3) ์ŠคํŠธ๋ฆผ๋ณ„ ๊ถŒ์žฅ๊ฐ’ (2-pass: reflux๋Š” P ๊ถŒ์žฅ ์ฐธ์กฐ) โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€ + var outs = new List(cfg.Streams.Count); + double? prodRec = null; + + foreach (var s in cfg.Streams) + { + if (s.RefluxFromProduct) continue; // pass2์—์„œ + var (rec, note) = ComputeStream(s, ff, dF, ts, st.Stream(s.Key)); + if (s.Key == cfg.ProductKey) prodRec = rec; + outs.Add(BuildAdvisory(s, pv, rec, note, transient, st.Stream(s.Key))); + } + foreach (var s in cfg.Streams) + { + if (!s.RefluxFromProduct) continue; + var stt = st.Stream(s.Key); + double? rec = null; + if (prodRec is double p) + { + var raw = Num.Clamp(s.TargetCoeff * p, s.SpMin, s.SpMax); + rec = stt.Rate.Step(raw, s.RateUpPerMin, s.RateDnPerMin, ts); + } + outs.Add(BuildAdvisory(s, pv, rec, "์™ธ๋ถ€ํ™˜๋ฅ˜ R=R_fร—P (P ์ง€์—ฐ ์ƒ์†)", transient, stt)); + } + + // โ”€โ”€ 4) ๋ฌผ์งˆ์ˆ˜์ง€ ๋ชจ๋‹ˆํ„ฐ (์ •์ƒ์ƒํƒœ์—์„œ๋งŒ, ยง10.6ยทยง11.6) โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€ + double? vloss = null, yield = null; + string mbState; + if (transient) + mbState = $"์ •์ฐฉ ๋Œ€๊ธฐ ({st.SettleTimerSec:F0}s)"; + else if (TryStreamPv(pv, "D", out var d) && TryStreamPv(pv, "P", out var pp2) + && TryStreamPv(pv, "B", out var b) && ff > 1e-6) + { + vloss = ff - (d + pp2 + b); + yield = 100.0 * pp2 / ff; + mbState = Math.Abs(vloss.Value) > 0.03 * ff ? "๋ฌผ์งˆ์ˆ˜์ง€ ๋ถˆ์ผ์น˜(๊ณ„์ธก ์ ๊ฒ€)" + : vloss.Value < 0 ? "์Œ์˜ ์†์‹ค(์ŠคํŒฌ ์˜ค๋ฅ˜ ์˜์‹ฌ)" + : "์ •์ƒ"; + } + else mbState = "์ž…๋ ฅ ๋ถ€์กฑ"; + + return new AdvisoryResult(cfg.Id, cfg.Name, now, cfg.Enabled, + transient, treason, ff, outs, vloss, yield, mbState); + } + + // โ”€โ”€ helpers โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€ + + private static (double? rec, string note) ComputeStream( + StreamConfig s, double ff, double dF, double ts, StreamState stt) + { + switch (s.Role) + { + case StreamRole.Commanded: + double theta = dF >= 0 ? s.ThetaUpSec : s.ThetaDnSec; // D7 ๋น„๋Œ€์นญ + double fd = stt.Dead.Through(ff, theta, ts); + fd = stt.Lag.Step(fd, s.TauSec, ts); + double raw = Num.Clamp(s.TargetCoeff * fd, s.SpMin, s.SpMax); + double rec = stt.Rate.Step(raw, s.RateUpPerMin, s.RateDnPerMin, ts); + return (rec, ""); + case StreamRole.LevelDriven: + return (s.TargetCoeff * ff, "๋ ˆ๋ฒจ ์ œ์–ด ๊ตฌ๋™ โ€” ๊ธฐ๋Œ€์น˜(deadtime ๋ฏธ์ ์šฉ)"); // ยง9.3 D5 + default: // Monitor + return (null, "๋ชจ๋‹ˆํ„ฐ(SP ์—†์Œ)"); + } + } + + private static StreamAdvisory BuildAdvisory( + StreamConfig s, PvSnapshot pv, double? rec, string note, bool transient, StreamState stt) + { + double curPv = pv.Streams.TryGetValue(s.Key, out var smp) && smp.Good ? smp.Value : double.NaN; + int trend = rec is double r && Num.IsFinite(stt.LastRec) + ? Math.Sign(r - stt.LastRec) : 0; + if (rec is double rr) stt.LastRec = rr; + double? gap = (rec is double g && Num.IsFinite(curPv)) ? g - curPv : null; + return new StreamAdvisory(s.Key, s.FlowTag, s.Role, curPv, rec, gap, trend, + valid: !transient && s.Role != StreamRole.Monitor, s.Grade, note); + } + + private static bool TryStreamPv(PvSnapshot pv, string key, out double v) + { + v = double.NaN; + if (pv.Streams.TryGetValue(key, out var s) && s.Good && Num.IsFinite(s.Value)) { v = s.Value; return true; } + return false; + } + + private static void SeedAll(ColumnConfig cfg, PvSnapshot pv, ColumnState st, double ff) + { + st.FeedDeriv.Update(ff, cfg.ScanSec); + foreach (var s in cfg.Streams) + { + var stt = st.Stream(s.Key); + double seed = pv.Streams.TryGetValue(s.Key, out var smp) && smp.Good ? smp.Value : ff * s.TargetCoeff; + stt.Lag.Seed(seed); + stt.Rate.Seed(seed); + stt.LastRec = seed; + } + } + + private AdvisoryResult Hold(ColumnConfig cfg, ColumnState st, DateTime now, string reason) + { + var outs = cfg.Streams.Select(s => + { + var stt = st.Stream(s.Key); + double? rec = Num.IsFinite(stt.LastRec) ? stt.LastRec : (double?)null; + return new StreamAdvisory(s.Key, s.FlowTag, s.Role, double.NaN, rec, null, 0, + valid: false, s.Grade, $"ํ™€๋“œ: {reason}"); + }).ToList(); + return new AdvisoryResult(cfg.Id, cfg.Name, now, cfg.Enabled, true, reason, + st.FeedFilter.Value, outs, null, null, $"ํ™€๋“œ: {reason}"); + } +} +``` + +--- + +## 5. ์ฝ”๋“œ โ€” ์ˆ˜๊ธ‰/์ €์žฅ/Supervisor + +### 5.1 Stores (`IFeedforwardStores.cs`) + +```csharp +namespace ExperionCrawler.Core.Application.Feedforward; + +public interface IFeedforwardConfigStore +{ + Task> LoadAllAsync(CancellationToken ct = default); +} + +public interface IFeedforwardAdvisoryStore +{ + void Set(AdvisoryResult result); + AdvisoryResult? Get(int columnId); + IReadOnlyCollection GetAll(); +} +``` + +### 5.2 In-memory advisory store (`FeedforwardAdvisoryStore.cs`) + +```csharp +using System.Collections.Concurrent; +using ExperionCrawler.Core.Application.Feedforward; + +namespace ExperionCrawler.Infrastructure.Control; + +public sealed class FeedforwardAdvisoryStore : IFeedforwardAdvisoryStore +{ + private readonly ConcurrentDictionary _latest = new(); + public void Set(AdvisoryResult r) => _latest[r.ColumnId] = r; // ๋‹จ์ผ writer(Supervisor) + public AdvisoryResult? Get(int id) => _latest.TryGetValue(id, out var r) ? r : null; + public IReadOnlyCollection GetAll() => _latest.Values.ToArray(); +} +``` + +### 5.3 Supervisor (`FeedforwardSupervisor.cs`) โ€” ์“ฐ๊ธฐ ์—†์Œ + +```csharp +using ExperionCrawler.Core.Application.Feedforward; +using ExperionCrawler.Core.Application.Interfaces; +using ExperionCrawler.Core.Domain.Entities; +using Microsoft.Extensions.DependencyInjection; +using Microsoft.Extensions.Hosting; +using Microsoft.Extensions.Logging; +using System.Globalization; + +namespace ExperionCrawler.Infrastructure.Control; + +public sealed class FeedforwardSupervisor : BackgroundService +{ + private readonly IServiceScopeFactory _scopeFactory; + private readonly FeedforwardEngine _engine; + private readonly IFeedforwardAdvisoryStore _store; + private readonly ILogger _logger; + private readonly Dictionary _states = new(); // ์ปฌ๋Ÿผ๋ณ„ ์ƒํƒœ(๋‹จ์ผ ๋ฃจํ”„ ์†Œ์œ ) + + public FeedforwardSupervisor( + IServiceScopeFactory scopeFactory, FeedforwardEngine engine, + IFeedforwardAdvisoryStore store, ILogger logger) + { _scopeFactory = scopeFactory; _engine = engine; _store = store; _logger = logger; } + + protected override async Task ExecuteAsync(CancellationToken ct) + { + // ๋ถ€ํŒ… ๋น„๋ธ”๋กœํ‚น: ์ž ๊น ์–‘๋ณด ํ›„ ์ง„์ž… (ExperionRealtimeService ํŒจํ„ด) + await Task.Yield(); + while (!ct.IsCancellationRequested) + { + double minScan = 2.0; + try + { + using var scope = _scopeFactory.CreateScope(); + var cfgStore = scope.ServiceProvider.GetRequiredService(); + var db = scope.ServiceProvider.GetRequiredService(); + + var columns = await cfgStore.LoadAllAsync(ct); + var enabled = columns.Where(c => c.Enabled).ToList(); + if (enabled.Count > 0) minScan = enabled.Min(c => c.ScanSec); + + foreach (var cfg in enabled) + { + try + { + var snap = await BuildSnapshotAsync(db, cfg); + var st = GetState(cfg.Id); + var res = _engine.Tick(cfg, snap, st, DateTime.UtcNow); + _store.Set(res); // โ˜… ์ €์žฅ๋งŒ โ€” ์“ฐ๊ธฐ ์—†์Œ + } + catch (Exception ex) + { + _logger.LogWarning(ex, "FF tick ์‹คํŒจ: column {Id}", cfg.Id); + } + } + } + catch (Exception ex) + { + _logger.LogError(ex, "FF supervisor ๋ฃจํ”„ ์˜ค๋ฅ˜"); + } + await Task.Delay(TimeSpan.FromSeconds(Math.Clamp(minScan, 1.0, 10.0)), ct); + } + } + + private ColumnState GetState(int id) + { + if (!_states.TryGetValue(id, out var s)) { s = new ColumnState(); _states[id] = s; } + return s; + } + + private async Task BuildSnapshotAsync(IExperionDbService db, ColumnConfig cfg) + { + // ํ•„์š”ํ•œ .pv ํƒœ๊ทธ ๋ชฉ๋ก ๊ตฌ์„ฑ + // โ˜… GetRealtimeRecordsByTagNamesAsync๋Š” tags.Contains(x.TagName) ์ •ํ™•ยท๋Œ€์†Œ๋ฌธ์ž๊ตฌ๋ถ„ ๋งค์นญ. + // realtime_table์€ ์†Œ๋ฌธ์ž ์ €์žฅ โ†’ ๋ฐ˜๋“œ์‹œ ์†Œ๋ฌธ์ž๋กœ ์งˆ์˜. + string PvTag(string baseTag) + { + var t = baseTag.ToLowerInvariant(); + return t.EndsWith(".pv") ? t : t + ".pv"; + } + var feedTag = PvTag(cfg.FeedTag); + var tags = new List { feedTag }; + if (cfg.PressureTag is not null) tags.Add(PvTag(cfg.PressureTag)); + tags.AddRange(cfg.LevelTags.Select(PvTag)); + tags.AddRange(cfg.Streams.Select(s => PvTag(s.FlowTag))); + + var rows = (await db.GetRealtimeRecordsByTagNamesAsync(tags)) // ๊ธฐ์กด ์ธํ„ฐํŽ˜์ด์Šค ์žฌ์‚ฌ์šฉ + .ToDictionary(r => r.TagName.ToLowerInvariant(), r => r); + + TagSample Sample(string baseTag) + { + var tag = PvTag(baseTag); + if (rows.TryGetValue(tag.ToLowerInvariant(), out var r) + && double.TryParse(r.LiveValue, NumberStyles.Float, CultureInfo.InvariantCulture, out var v)) + { + bool fresh = (DateTime.UtcNow - r.Timestamp.ToUniversalTime()).TotalSeconds <= cfg.StaleSec; + return new TagSample(tag, v, Good: fresh, r.Timestamp); + } + return new TagSample(tag, double.NaN, Good: false, DateTime.MinValue); + } + + var feed = Sample(cfg.FeedTag); + var press = cfg.PressureTag is null ? null : Sample(cfg.PressureTag); + var levels = cfg.LevelTags.Select(Sample).ToList(); + var streams = cfg.Streams.ToDictionary(s => s.Key, s => Sample(s.FlowTag)); + return new PvSnapshot(feed, press, levels, streams); + } +} +``` + +> โœ… **ํ™•์ธ๋จ**: `RealtimePoint`(`src/Core/Domain/Entities/ExperionEntities.cs:72`)๋Š” +> `TagName`ยท`LiveValue`(string?, `[Column("livevalue")]`)ยท`Timestamp`ยท`NodeId`๋ฅผ ๊ฐ€์ง„๋‹ค. ๋ณธ ์ฝ”๋“œ์˜ ์‚ฌ์šฉ๊ณผ ์ผ์น˜. + +### 5.4 Config DDL + ๋กœ๋” (`FeedforwardConfigStore.cs`) + +**DDL ์‚ฝ์ž… ์œ„์น˜(G4)**: `src/Infrastructure/Database/ExperionDbContext.cs` ์˜ **`ExperionDbService.InitializeAsync`** +(ํด๋ž˜์Šค `ExperionDbService`, ์•ฝ line 285~). ๊ธฐ์กด `await _ctx.Database.ExecuteSqlRawAsync("""...""")` ๋ธ”๋ก๋“ค์ด +๋‚˜์—ด๋œ ๋๋ถ€๋ถ„(์˜ˆ: ๋งˆ์ง€๋ง‰ ๋ทฐ ์ƒ์„ฑ ๋’ค, `return true;` ์ง์ „)์— ์•„๋ž˜ ๋‘ ํ…Œ์ด๋ธ” ์ƒ์„ฑ์„ **๋ฉฑ๋“ฑ ์ถ”๊ฐ€**ํ•œ๋‹ค. + +```sql +-- ExperionDbService.InitializeAsync ๋‚ด, ๊ธฐ์กด ExecuteSqlRawAsync ๋ธ”๋ก๋“ค ๋์— ์ถ”๊ฐ€ (๋ฉฑ๋“ฑ) +CREATE TABLE IF NOT EXISTS ff_column_config ( + id SERIAL PRIMARY KEY, + name TEXT NOT NULL, + enabled BOOLEAN NOT NULL DEFAULT FALSE, + feed_tag TEXT NOT NULL, + pressure_tag TEXT, + level_tags TEXT, -- ์ฝค๋งˆ ๊ตฌ๋ถ„ + scan_sec DOUBLE PRECISION NOT NULL DEFAULT 2, + feed_filter_tau_sec DOUBLE PRECISION NOT NULL DEFAULT 300, + feed_move_thr_per_min DOUBLE PRECISION NOT NULL DEFAULT 0, + press_filter_tau_sec DOUBLE PRECISION NOT NULL DEFAULT 60, + pressure_band DOUBLE PRECISION NOT NULL DEFAULT 1e9, + settle_sec DOUBLE PRECISION NOT NULL DEFAULT 0, + stale_sec DOUBLE PRECISION NOT NULL DEFAULT 120, + product_key TEXT NOT NULL DEFAULT 'P', + advisory_only BOOLEAN NOT NULL DEFAULT TRUE -- Phase I ๊ฐ•์ œ +); +CREATE TABLE IF NOT EXISTS ff_stream_config ( + id SERIAL PRIMARY KEY, + column_id INTEGER NOT NULL REFERENCES ff_column_config(id) ON DELETE CASCADE, + key TEXT NOT NULL, + flow_tag TEXT NOT NULL, + role TEXT NOT NULL, -- Commanded|LevelDriven|Monitor + target_coeff DOUBLE PRECISION NOT NULL DEFAULT 0, + theta_up_sec DOUBLE PRECISION NOT NULL DEFAULT 0, + theta_dn_sec DOUBLE PRECISION NOT NULL DEFAULT 0, + tau_sec DOUBLE PRECISION NOT NULL DEFAULT 0, + sp_min DOUBLE PRECISION NOT NULL DEFAULT 0, + sp_max DOUBLE PRECISION NOT NULL DEFAULT 1e9, + rate_up_per_min DOUBLE PRECISION NOT NULL DEFAULT 1e9, + rate_dn_per_min DOUBLE PRECISION NOT NULL DEFAULT 1e9, + reflux_from_product BOOLEAN NOT NULL DEFAULT FALSE, + grade TEXT NOT NULL DEFAULT 'A' +); +``` + +> ๋‘ `CREATE TABLE` ๋ฌธ์€ ๊ฐ๊ฐ ๋ณ„๋„์˜ `await _ctx.Database.ExecuteSqlRawAsync("""...""")` ํ˜ธ์ถœ๋กœ ๋„ฃ๋Š”๋‹ค +> (EF์˜ ๋‹ค์ค‘๋ฌธ์žฅ ์ œ์•ฝ ํšŒํ”ผ โ€” ๊ธฐ์กด ์ฝ”๋“œ๋„ ํ•œ ํ˜ธ์ถœ์— ํ•œ ๋ฌธ์žฅ). + +**๋กœ๋” ์ „์ฒด ์ฝ”๋“œ** โ€” EF `ExperionDbContext`์˜ ADO ์ปค๋„ฅ์…˜ ์‚ฌ์šฉ(์‹ ๊ทœ ์—”ํ‹ฐํ‹ฐ ๋งคํ•‘ ๋ถˆํ•„์š”, ์‚ฌ์šฉ์ž ์ž…๋ ฅ ์—†์Œโ†’์ธ์ ์…˜ ์—†์Œ): + +```csharp +using System.Data; +using ExperionCrawler.Core.Application.Feedforward; +using ExperionCrawler.Infrastructure.Database; // ExperionDbContext +using Microsoft.EntityFrameworkCore; // GetDbConnection() +using Microsoft.Extensions.Logging; + +namespace ExperionCrawler.Infrastructure.Control; + +public sealed class FeedforwardConfigStore : IFeedforwardConfigStore +{ + private readonly ExperionDbContext _ctx; + private readonly ILogger _logger; + + public FeedforwardConfigStore(ExperionDbContext ctx, ILogger logger) + { _ctx = ctx; _logger = logger; } + + public async Task> LoadAllAsync(CancellationToken ct = default) + { + var conn = _ctx.Database.GetDbConnection(); + if (conn.State != ConnectionState.Open) await conn.OpenAsync(ct); + + // 1) ์ปฌ๋Ÿผ + var cols = new Dictionary streams)>(); + await using (var cmd = conn.CreateCommand()) + { + cmd.CommandText = """ + SELECT id, name, enabled, feed_tag, pressure_tag, level_tags, scan_sec, + feed_filter_tau_sec, feed_move_thr_per_min, + press_filter_tau_sec, pressure_band, settle_sec, + stale_sec, product_key + FROM ff_column_config + """; + await using var rd = await cmd.ExecuteReaderAsync(ct); + while (await rd.ReadAsync(ct)) + { + var levelTags = rd.IsDBNull(5) + ? Array.Empty() + : rd.GetString(5) + .Split(',', StringSplitOptions.RemoveEmptyEntries | StringSplitOptions.TrimEntries) + .Select(t => t.ToLowerInvariant()).ToArray(); + + var cfg = new ColumnConfig + { + Id = rd.GetInt32(0), + Name = rd.GetString(1), + Enabled = rd.GetBoolean(2), + AdvisoryOnly = true, // โ˜… ๋ถˆ๋ณ€์‹ ๊ฐ•์ œ(DB ๋ฌด๊ด€) + FeedTag = rd.GetString(3).ToLowerInvariant(), + PressureTag = rd.IsDBNull(4) ? null : rd.GetString(4).ToLowerInvariant(), + LevelTags = levelTags, + ScanSec = rd.GetDouble(6), + FeedFilterTauSec = rd.GetDouble(7), + FeedMoveThresholdPerMin = rd.GetDouble(8), + PressFilterTauSec = rd.GetDouble(9), + PressureBand = rd.GetDouble(10), + SettleSec = rd.GetDouble(11), + StaleSec = rd.GetDouble(12), + ProductKey = rd.GetString(13), + Streams = Array.Empty() + }; + cols[cfg.Id] = (cfg, new List()); + } + } + + // 2) ์ŠคํŠธ๋ฆผ + await using (var cmd = conn.CreateCommand()) + { + cmd.CommandText = """ + SELECT column_id, key, flow_tag, role, target_coeff, theta_up_sec, theta_dn_sec, + tau_sec, sp_min, sp_max, rate_up_per_min, rate_dn_per_min, + reflux_from_product, grade + FROM ff_stream_config + ORDER BY id + """; + await using var rd = await cmd.ExecuteReaderAsync(ct); + while (await rd.ReadAsync(ct)) + { + int colId = rd.GetInt32(0); + if (!cols.TryGetValue(colId, out var entry)) continue; + entry.streams.Add(new StreamConfig + { + Key = rd.GetString(1), + FlowTag = rd.GetString(2).ToLowerInvariant(), + Role = Enum.TryParse(rd.GetString(3), true, out var role) ? role : StreamRole.Monitor, + TargetCoeff = rd.GetDouble(4), + ThetaUpSec = rd.GetDouble(5), + ThetaDnSec = rd.GetDouble(6), + TauSec = rd.GetDouble(7), + SpMin = rd.GetDouble(8), + SpMax = rd.GetDouble(9), + RateUpPerMin = rd.GetDouble(10), + RateDnPerMin = rd.GetDouble(11), + RefluxFromProduct = rd.GetBoolean(12), + Grade = Enum.TryParse(rd.GetString(13), true, out var g) ? g : Confidence.A + }); + } + } + + return cols.Values.Select(e => e.cfg with { Streams = e.streams }).ToList(); + } +} +``` + +> `GetDouble`/`GetBoolean` ์ปฌ๋Ÿผ์€ DDL์ด `DOUBLE PRECISION`/`BOOLEAN`์ด๋ผ ํƒ€์ž… ์ผ์น˜. NULL์€ ์œ„ DDL `NOT NULL DEFAULT`๋กœ ๋ฐฉ์ง€. +> ์ปค๋„ฅ์…˜์€ EF๊ฐ€ ์†Œ์œ ํ•˜๋ฏ€๋กœ **๋‹ซ์ง€ ์•Š๋Š”๋‹ค**(์—ด๊ธฐ๋งŒ ๋ณด์žฅ). + +### 5.5 Controller (`FeedforwardController.cs`) โ€” ์ฝ๊ธฐ (camelCase ํ•„์ˆ˜) + +```csharp +using ExperionCrawler.Core.Application.Feedforward; +using Microsoft.AspNetCore.Mvc; + +namespace ExperionCrawler.Web.Controllers; + +[ApiController] +[Route("api/ff")] +public sealed class FeedforwardController : ControllerBase +{ + private readonly IFeedforwardAdvisoryStore _store; + public FeedforwardController(IFeedforwardAdvisoryStore store) => _store = store; + + [HttpGet("advisory")] + public IActionResult GetAll() => Ok(new + { + columns = _store.GetAll().Select(MapColumn) + }); + + [HttpGet("advisory/{columnId:int}")] + public IActionResult Get(int columnId) + { + var r = _store.Get(columnId); + return r is null ? NotFound() : Ok(MapColumn(r)); + } + + // CODING_CONVENTIONS ยง1: ๋ชจ๋“  ํ‚ค camelCase ๋ช…์‹œ (shorthand/typed ๋ฐ˜ํ™˜ ๊ธˆ์ง€) + private static object MapColumn(AdvisoryResult r) => new + { + columnId = r.ColumnId, + columnName = r.ColumnName, + computedAt = r.ComputedAt, + enabled = r.Enabled, + transient = r.Transient, + transientReason = r.TransientReason, + feedFiltered = r.FeedFiltered, + vLoss = r.VLoss, + yield = r.Yield, + massBalanceState = r.MassBalanceState, + streams = r.Streams.Select(s => new + { + key = s.Key, + flowTag = s.FlowTag, + role = s.Role.ToString(), + pv = double.IsNaN(s.Pv) ? (double?)null : s.Pv, + recommendedSp = s.RecommendedSp, + gap = s.Gap, + trend = s.Trend, + valid = s.Valid, + grade = s.Grade.ToString(), + note = s.Note + }) + }; +} +``` + +### 5.6 DI ๋“ฑ๋ก (์ง„๋‹จ ํ†ต๊ณผ ํ›„์—๋งŒ `Program.cs`์— ์ถ”๊ฐ€) + +```csharp +builder.Services.AddSingleton(); +builder.Services.AddSingleton(); +builder.Services.AddScoped(); +builder.Services.AddHostedService(); +``` + +### 5.7 ํ…Œ์ŠคํŠธ ํ”„๋กœ์ ํŠธ ์Šค์บํด๋“œ (G3) โ€” C# ํ…Œ์ŠคํŠธ ์ธํ”„๋ผ๊ฐ€ ์—†์œผ๋ฏ€๋กœ ์‹ ์„ค + +์ €์žฅ์†Œ์— C# ํ…Œ์ŠคํŠธ ํ”„๋กœ์ ํŠธ๊ฐ€ **์—†๋‹ค**(Python pytest๋งŒ ์กด์žฌ). ์ˆœ์ˆ˜ ๋ธ”๋กยท์—”์ง„ ๊ฒ€์ฆ์šฉ xUnit ํ”„๋กœ์ ํŠธ๋ฅผ ์‹ ์„คํ•œ๋‹ค. + +```bash +# 1) ํ…Œ์ŠคํŠธ ํ”„๋กœ์ ํŠธ ์ƒ์„ฑ + ์•ฑ ํ”„๋กœ์ ํŠธ ์ฐธ์กฐ (์†”๋ฃจ์…˜ ํŒŒ์ผ ์—†์Œ โ†’ ์ง์ ‘ ์ฐธ์กฐ) +dotnet new xunit -o tests/ExperionCrawler.Tests +dotnet add tests/ExperionCrawler.Tests/ExperionCrawler.Tests.csproj \ + reference src/Web/ExperionCrawler.csproj +# 2) ์‹คํ–‰ +dotnet test tests/ExperionCrawler.Tests/ExperionCrawler.Tests.csproj +``` + +> Web SDK ํ”„๋กœ์ ํŠธ๋„ ํ…Œ์ŠคํŠธ์—์„œ ์ฐธ์กฐ ๊ฐ€๋Šฅ. ํ…Œ์ŠคํŠธ ๋Œ€์ƒ ํด๋ž˜์Šค(`FeedforwardEngine`ยท์—ฐ์‚ฐ๋ธ”๋ก)๋Š” ๋ชจ๋‘ `public`. +> DBยทOPC ์˜์กด์ด ์—†๋Š” **์ˆœ์ˆ˜ ๋กœ์ง๋งŒ** ํ…Œ์ŠคํŠธ(SupervisorยทConfigStore๋Š” ํ†ตํ•ฉ๊ฒ€์ฆ ยง8.2์—์„œ ์ˆ˜๋™). + +**์˜ˆ์‹œ ํ…Œ์ŠคํŠธ(`FeedforwardBlocksTests.cs`) โ€” ๊ฐ€์žฅ ๊นŒ๋‹ค๋กœ์šด 3๊ฐœ:** + +```csharp +using ExperionCrawler.Infrastructure.Control; +using Xunit; + +public class FeedforwardBlocksTests +{ + [Fact] + public void DeadTime_delays_by_n_samples() + { + var d = new DeadTimeBuffer(); + // ฮธ=10s, ts=2s โ†’ n=5 ์Šค์บ” ์ง€์—ฐ. ๋ฒ„ํผ๋Š” ์ฒซ ์ž…๋ ฅ(10)์œผ๋กœ ์‹œ๋“œ. + Assert.Equal(10.0, d.Through(10, 10, 2), 3); // call1 ์‹œ๋“œ โ†’ 10 + for (int i = 0; i < 4; i++) // call2~5: ์ž…๋ ฅ 20, ์ถœ๋ ฅ์€ ์˜›๊ฐ’ 10 + Assert.Equal(10.0, d.Through(20, 10, 2), 3); + Assert.Equal(10.0, d.Through(20, 10, 2), 3); // call6: ์•„์ง 10 (์ง€์—ฐ 5) + Assert.Equal(20.0, d.Through(20, 10, 2), 3); // call7: ๋น„๋กœ์†Œ 20 ๋“ฑ์žฅ + } + + [Fact] // ์ง„๋‹จ ์ •์ • ํšŒ๊ท€: ๋น„๋Œ€์นญ ฮธ ํ† ๊ธ€์—๋„ ์ง€์—ฐ์„  ๋ณด์กด + public void DeadTime_asymmetric_theta_preserves_history() + { + var d = new DeadTimeBuffer(); double ts = 2; + for (int i = 0; i < 12; i++) d.Through(0, 20, ts); // ฮธ=20โ†’n=10, 0 ์ถฉ์ „ + Assert.Equal(0.0, d.Through(100, 10, ts), 3); // ฮธ=10โ†’n=5, ์•„์ง 0(๋ˆ„์ถœ ์—†์Œ) + Assert.Equal(0.0, d.Through(100, 20, ts), 3); // ฮธ ๋ณต๊ท€ํ•ด๋„ 0 + } + + [Fact] + public void RateLimiter_clamps_asymmetric_up_down() + { + var r = new RateLimiter(); + r.Seed(0); + // up=60/min, ts=1s โ†’ ์Šค์บ”๋‹น +1 ํ•œ๋„ + Assert.Equal(1, r.Step(100, 60, 600, 1), 3); + // dn=600/min โ†’ ์Šค์บ”๋‹น -10 ํ•œ๋„ + r.Seed(100); + Assert.Equal(90, r.Step(0, 60, 600, 1), 3); + } + + [Fact] + public void FirstOrderLag_reaches_63pct_after_tau() + { + var l = new FirstOrderLag(); + l.Seed(0); + double y = 0; double ts = 1, tau = 10; + for (int i = 0; i < 10; i++) y = l.Step(1.0, tau, ts); // ฯ„=10s, 10์Šค์บ” + Assert.InRange(y, 0.60, 0.66); // โ‰ˆ63% + } +} +``` + +### 5.8 ์‹คํ–‰์šฉ ์˜ˆ์‹œ ์„ค์ • (G5) โ€” C-6111 seed + +end-to-end ๊ฐ€๋™ ๊ฒ€์ฆ์„ ์œ„ํ•œ **placeholder seed**. ฮธ/ฯ„๋Š” ยง11.4 ์ „ํ˜•๊ฐ’, ํ•œ๊ณ„๋Š” ๋Œ€๋žต์น˜ โ€” +**์‹ค์ธกยท์šด์ „์› ๊ฐ’์œผ๋กœ ๊ต์ฒด ์ „๊นŒ์ง€ ์ž„์‹œ**(Phase II Web UI ๊ณต๊ธ‰). ํƒœ๊ทธ๋Š” ยงํ™•์ธ๋œ ์‹ค์žฌ ํƒœ๊ทธ. + +```sql +-- C-6111 (P6-1) advisory 1์ปฌ๋Ÿผ. ์ง„๋‹จยทDI ๋“ฑ๋ก ํ›„ 1ํšŒ ์‹คํ–‰. +INSERT INTO ff_column_config + (name, enabled, feed_tag, pressure_tag, level_tags, scan_sec, + feed_filter_tau_sec, feed_move_thr_per_min, pressure_band, settle_sec, stale_sec, product_key) +VALUES + ('C-6111', TRUE, 'ficq-6101', 'pica-6111', 'li-6111,lica-6113', 2, + 300, 5, 3, 1800, 120, 'P') +RETURNING id; +-- ์œ„ id๋ฅผ :cid ๋กœ ์‚ฌ์šฉ (์˜ˆ์‹œ๋Š” 1๋กœ ๊ฐ€์ •) + +-- ์ŠคํŠธ๋ฆผ: P=commanded, R=reflux(P ๊ฒฝ์œ ), DยทB=level_driven(LICA-6113 ์ธ๋ฒคํ† ๋ฆฌ ๊ตฌ๋™), ๋ชจ๋‹ˆํ„ฐ ์—†์Œ +INSERT INTO ff_stream_config + (column_id, key, flow_tag, role, target_coeff, theta_up_sec, theta_dn_sec, tau_sec, + sp_min, sp_max, rate_up_per_min, rate_dn_per_min, reflux_from_product, grade) VALUES + (1, 'P', 'ficq-6118', 'Commanded', 0.95, 60, 60, 900, 0, 950, 30, 60, FALSE, 'A'), + (1, 'R', 'ficq-6113', 'Commanded', 0.80, 0, 0, 0, 0,1100, 30, 30, TRUE, 'A'), + (1, 'D', 'ficq-6114', 'LevelDriven', 0.02, 0, 0, 0, 0, 60, 0, 0, FALSE, 'B'), + (1, 'B', 'ficq-6116', 'LevelDriven', 0.03, 0, 0, 0, 0, 80, 0, 0, FALSE, 'B'); +``` + +> **placeholder ๊ฒฝ๊ณ **: ์œ„ ฮธ(P=60s)ยทฯ„(P=900s=15min)ยทrateยทsp_max๋Š” **์ž„์‹œ ์ถ”์ •**. ยง11.4 bump test/Phase II๋กœ ๊ต์ฒด. +> DยทB๋Š” `LevelDriven`์ด๋ผ deadtime/rate ๋ฏธ์ ์šฉ(๊ธฐ๋Œ€์น˜ KยทF๋งŒ ํ‘œ์‹œ). reflux R์€ P ๊ถŒ์žฅ๊ฐ’ ๊ฒฝ์œ (ยง4 ์—”์ง„). + +--- + +## 6. ๋‚˜๋จธ์ง€ ํ•ญ๋ชฉ ๋ณด์ • ํ”Œ๋žœ (ยง13ยทยง14 โ†’ Phase II/III) + +| ID | ํ•ญ๋ชฉ | ๊ทผ๊ฑฐ(ยง) | Phase | ๊ตฌํ˜„ ๋ฐฉํ–ฅ | +|:--:|:-----|:------:|:-----:|:----------| +| P-1 | **ฮธ ์ž๋™ํŠœ๋‹** (passive ๊ต์ฐจ์ƒ๊ด€) | ยง13.4 | II | `CrossCorrLagEstimator`: ฮ”Fยทฮ”S(=TICA.OP) ๋‹ค์ž…๋ ฅ ๋ถ€๋ถ„์ƒ๊ด€ โ†’ ฮธ_i + ์‹ ๋ขฐ๋„. StreamConfig.ฮธ๋ฅผ *์ œ์•ˆ*๋งŒ(์šด์ „์› ์Šน์ธ ์‹œ ๋ฐ˜์˜). ํ๋ฃจํ”„ ์˜ค์—ผ ํšŒํ”ผ(์ŠคํŒ€ 2nd ์ž…๋ ฅ) | +| P-2 | **PCT/์ฐจ์˜จ ๋ชจ๋‹ˆํ„ฐ** | ยง13.3ยทยง14.1 | II | `TempCorrection`(ๅทฒ๊ตฌํ˜„) + `DiffTemp`. PCT๋Š” dT/dPยทPref **๊ณ„์ ˆ ์žฌ์บ˜๋ฆฌ๋ธŒ๋ ˆ์ด์…˜**. ์ปฌ๋Ÿผ ์„ค์ •์— tempTagsยทdTdPยทpRef ์ถ”๊ฐ€ | +| P-3 | **Sweet-spot/ํ”„๋ก ํŠธ ์œ„์น˜ ์ง€ํ‘œ** | ยง13.5 | II | `FrontPositionIndicator`: ๋ฏผ๊ฐํŠธ๋ ˆ์ด PCT/ฮ”T โ†’ ๋“œ๋ฆฌํ”„ํŠธ ์‹œ ํ™˜๋ฅ˜/boilup ํŠธ๋ฆผ *๊ถŒ์žฅ*(advisory). analyzer ์žˆ์œผ๋ฉด ์šฐ์„  | +| P-4 | **๋А๋ฆฐ ๋ฐ”์ด์–ด์Šค ์ ์‘** (Kยทk_V) | ยง14.4 | II | ๋ฌผ์งˆ์ˆ˜์ง€ **์žฅ๊ธฐ ์ด๋™ํ‰๊ท **์œผ๋กœ K_obsยทk_V ์ถ”์„ธ ์‚ฐ์ถœ โ†’ ์šด์ „์›์—๊ฒŒ "๊ณ„์ ˆ ๋ณด์ • ์ œ์•ˆ". ์ž๋™ ๋ณ€๊ฒฝ ์•„๋‹˜ | +| P-5 | **confidence ์ž๋™ ๊ฐ•๋“ฑ** | ยง14.3 | II | ์ž…๋ ฅ ์‹ ์„ ๋„ยท์••๋ ฅ์•ˆ์ •ยทanalyzer ๋ถ€์žฌ ๋“ฑ์œผ๋กœ Aโ†’Bโ†’C ๋™์  ๊ฐ•๋“ฑ, UI ์ƒ‰์ƒ | +| P-6 | **Web UI** ์„ค์ •/๋Œ€์‹œ๋ณด๋“œ(Tab 18) | ยง12.7 | II | ์ปฌ๋Ÿผ๋ณ„ ๊ฒฝํ—˜์ƒ์ˆ˜ ํผ(admin) + ๊ถŒ์žฅ SP ์นด๋“œ(ํ˜„์žฌ/๊ถŒ์žฅ/ฮ”/์ถ”์„ธ/valid). Tab16/17 ํŒจํ„ด. `press_filter_tau_sec` ํฌํ•จ โ€” ์••๋ ฅ 1์ฐจ์ €์—ญํ†ต๊ณผ ์‹œ์ •์ˆ˜(์ดˆ), ์šด์ „์ž๊ฐ€ ์••๋ ฅ๋…ธ์ด์ฆˆ ํŠน์„ฑ์— ๋งž์ถฐ ์กฐ์ • | +| P-7 | **RSP ๊ฐ๋…์ œ์–ด ์“ฐ๊ธฐ** | ยง12.8 Stage3 | III | `WriteGuard`(min/maxยทrateยทฮ”cap) + ์›Œ์น˜๋…ยท๋ฐ๋“œ๋งจ + ์šด์ „์› ์ฑ„ํƒ ์Šค์œ„์น˜. `ExperionOpcWriteClient` ๊ฐ€๋“œ 0๊ฑด โ†’ ์„ ํ–‰ ๊ตฌํ˜„ ํ•„์ˆ˜ | + +> Phase I ์ฝ”๋“œ๋Š” P-1~P-7์˜ **ํ™•์žฅ์ (์„ค์ • ํ•„๋“œยท๋ธ”๋ก ์ธํ„ฐํŽ˜์ด์Šค)** ์„ ๋‚จ๊ฒจ๋‘๋˜, **๋ณธ Phase์—์„  ๋ฏธ๊ตฌํ˜„**. + +--- + +## 7. ๊ฒ€์ฆ ์ ˆ์ฐจ (diagnosis-checklist.md 8๋‹จ๊ณ„ ๊ธฐ์ค€) โ€” ๊ฐ๋…์ž ์ง„๋‹จ์šฉ + +> ๊ฐ๋…์ž๋Š” ์•„๋ž˜๋ฅผ **์ˆœ์„œ๋Œ€๋กœ** ์ˆ˜ํ–‰ํ•˜๊ณ , ๋ฐœ๊ฒฌ ํ•ญ๋ชฉ์„ STEP 8 ๋ณด๊ณ ์„œ ์–‘์‹์œผ๋กœ ๊ธฐ๋กํ•œ๋‹ค. +> ๋ณธ ์ฝ”๋“œ๋Š” **์‹ ๊ทœ ํŒŒ์ผ**์ด๋ฏ€๋กœ STEP 3์˜ "์ „์ฒด ํŒŒ์ผ ์ฝ๊ธฐ"๋Š” ยง2~ยง5 ์ฝ”๋“œ๋ธ”๋ก ์ „์ฒด๋ฅผ ๋Œ€์ƒ์œผ๋กœ ํ•œ๋‹ค. + +### STEP 1 โ€” ๋งฅ๋ฝ +- ๋ ˆ์ด์–ด: Core(๋ชจ๋ธ/์ธํ„ฐํŽ˜์ด์Šค), Infrastructure/Control(๋ธ”๋กยท์—”์ง„ยทSupervisor), Web(์ฝ๊ธฐ ์ปจํŠธ๋กค๋Ÿฌ). +- ๊ด€๋ จ ๋ฌธ์„œ: `์ธก๋ฅ˜์ถ”์ถœ์‹-ํ†ตํ•ฉ์œ ๋Ÿ‰์„ค์ •๊ณต์‹.md` ยง9~ยง14, `์ธก๋ฅ˜์ถ”์ถœ-์‹œ๊ฐ„์ง€์—ฐ-์ ์šฉ๋ฐฉ์‹.md`, ๋ณธ ๋ฌธ์„œ ยง0 Scope. +- **์˜๋„์  ์„ค๊ณ„ ํ™•์ธ**: โ‘  ์“ฐ๊ธฐ ์—†์Œ(advisory) โ‘ก DยทB๋Š” LevelDriven โ‘ข ์—”์ง„ ์ƒํƒœ๋Š” ๋‹จ์ผ ๋ฃจํ”„ ์†Œ์œ (๋ฝ ์—†์Œ) โ‘ฃ AdvisoryOnly ๊ฐ•์ œ. + +### STEP 2 โ€” ๊ตฌ์กฐ +- ์‹ ๊ทœ ํŒŒ์ผ ๋ชฉ๋ก(ยง1) ํ™•์ธ, ๊ธฐ์กด ํŒŒ์ผ ๋ณ€๊ฒฝ์€ **DI ๋“ฑ๋ก + ๋ถ€ํŠธ DDL 2ํ…Œ์ด๋ธ”**๋ฟ(๋‚˜๋จธ์ง€ ๋ฌด๋ณ€๊ฒฝ). +- ์˜์กด: `IExperionDbService.GetRealtimeRecordsByTagNamesAsync`, `RealtimePoint`, `ExperionDbContext`. + +### STEP 3 โ€” ์ฝ”๋“œ ์ฝ๊ธฐ (์ „์ฒด, ๊ฑด๋„ˆ๋›ฐ๊ธฐ ๊ธˆ์ง€) +์ˆœ์„œ: ๋ชจ๋ธ(ยง2) โ†’ ๋ธ”๋ก(ยง3) โ†’ ์—”์ง„(ยง4) โ†’ stores/supervisor(ยง5) โ†’ ์ปจํŠธ๋กค๋Ÿฌ(ยง5.5). + +### STEP 4 โ€” ํ˜ธ์ถœ ๊ณ„์ธต ์ง€๋„ +``` +BackgroundService.ExecuteAsync (๋ฃจํ”„) + โ†’ scope: IFeedforwardConfigStore.LoadAllAsync (DB read) + โ†’ scope: IExperionDbService.GetRealtimeRecordsByTagNamesAsync (DB read) + โ†’ FeedforwardEngine.Tick (์ˆœ์ˆ˜, I/O ์—†์Œ) โ† try-catch๋Š” ์ปฌ๋Ÿผ ๋‹จ์œ„ + ๋ฃจํ”„ ์ „์ฒด + โ†’ IFeedforwardAdvisoryStore.Set (in-memory) +HTTP GET /api/ff/advisory โ†’ AdvisoryStore.Get(All) (read only) +โ”€ ์“ฐ๊ธฐ ๊ฒฝ๋กœ ์—†์Œ (ExperionOpcWriteClient ๋ฏธ์ฐธ์กฐ) โ”€ +``` + +### STEP 5 โ€” ํŒจํ„ด ๋งค์นญ (์ž๊ฐ€ ์‚ฌ์ „์ ๊ฒ€ ๊ฒฐ๊ณผ ๋™๋ด‰) + +| ์ฒดํฌ | ๋ณธ ์ฝ”๋“œ ์ƒํƒœ | ์ฒ˜๋ฆฌ | +|:-----|:-------------|:-----| +| ๋ฏธ์ •์˜ ์ฐธ์กฐ | `RealtimePoint.LiveValue`/`TagName`/`Timestamp` โœ… ์‹ค์ œ ์—”ํ‹ฐํ‹ฐ(`ExperionEntities.cs:72`)์™€ ์ผ์น˜ ํ™•์ธ | OK | +| async ๋‚ด blocking | DB๋Š” `await`, ์—”์ง„์€ ์ˆœ์ˆ˜ | OK | +| Race condition | ์—”์ง„ ์ƒํƒœ๋Š” Supervisor ๋‹จ์ผ ๋ฃจํ”„ ์†Œ์œ , AdvisoryStore๋Š” ๋‹จ์ผ writer + ConcurrentDictionary | OK | +| `Task.Delay` ๊ณ ์ •๊ฐ’ | ๋ฃจํ”„ ์ฃผ๊ธฐ๋Š” cfg.ScanSec ๊ธฐ๋ฐ˜(clamp 1~10s) | OK(ํด๋ง ๋ณธ์งˆ) | +| DB ์ปค๋„ฅ์…˜ ๋ˆ„์ˆ˜ | `using var scope` per iteration | OK | +| ์˜ˆ์™ธ ์‚ผํ‚ด | ์ปฌ๋Ÿผ ๋‹จ์œ„ `LogWarning`, ๋ฃจํ”„ `LogError` | OK | +| 0 ๋‚˜๋ˆ—์…ˆ | `ff>1e-6` ๊ฐ€๋“œ, `Max(1e-6,..)` | OK | +| NaN ์ „ํŒŒ | `Num.IsFinite` ๊ฒŒ์ดํŠธ, BADโ†’Hold | OK | +| ์„ค์ • ํ•˜๋“œ์ฝ”๋”ฉ | ์ƒ์ˆ˜๋Š” cfg/DB | OK (๋‹จ, Resize seedยท~~press filter ฯ„=60 ํ•˜๋“œ์ฝ”๋”ฉ~~ โ†’ ํ•ด์†Œ: `PressFilterTauSec` ColumnConfig ์†์„ฑ์œผ๋กœ ์Šน๊ฒฉ, DDLยท๋กœ๋”ยท์—”์ง„ยทWeb UI(Tab 18) ์—ฐ๋™) | +| SQL Injection | ConfigStore๋Š” **์‚ฌ์šฉ์ž ์ž…๋ ฅ ์—†์ด ์ „์ฒด ํ–‰ SELECT**(WHERE/ํŒŒ๋ผ๋ฏธํ„ฐ ์—†์Œ) | OK (์ธ์ ์…˜ ํ‘œ๋ฉด ์—†์Œ) | +| **์“ฐ๊ธฐ ๋ถˆ๋ณ€์‹** | ์ฝ”๋“œ ์ „์ฒด์— SP/OP write ํ˜ธ์ถœ **์—†์Œ** | โ˜… ๊ฐ๋…์ž grep ํ™•์ธ | + +### STEP 6 โ€” ๊ต์ฐจ๊ฒ€์ฆ (์˜์‹ฌ ํ•ญ๋ชฉ๋ณ„ Q1~Q4) +- `RealtimePoint` ์†์„ฑ๋ช…: โœ… ํ™•์ธ ์™„๋ฃŒ(`ExperionEntities.cs:72`, `LiveValue`/`TagName`/`Timestamp` ์ผ์น˜) โ†’ ๋ณด๊ณ ์„œ ์ œ์™ธ. +- press filter ฯ„=60 ํ•˜๋“œ์ฝ”๋”ฉ: Q4(์žฅ์•  ์‹œ๋‚˜๋ฆฌ์˜ค?) โ†’ ์—†์Œ โ†’ **LOW**. +- ConfigStore ์ธ์ ์…˜: ์‚ฌ์šฉ์ž ์ž…๋ ฅ ์—†๋Š” ์ „์ฒด SELECT โ†’ ํ‘œ๋ฉด ์—†์Œ โ†’ ๋ณด๊ณ ์„œ ์ œ์™ธ. +- `DeadTimeBuffer` ์ง€์—ฐ ์ •ํ™•๋„: ๋‹จ์œ„ํ…Œ์ŠคํŠธ(ยง5.7)๋กœ n-์ƒ˜ํ”Œ ์ง€์—ฐ ๊ฒ€์ฆ โ†’ write-ํ›„-read๋Š” nโˆ’1 ๋ฒ„๊ทธ(์ด๋ฏธ ์ •์ •). + +### STEP 7 โ€” ์‹ฌ๊ฐ๋„ (๊ฐ๋…์ž ํŒ์ •) +- HIGH: ๋นŒ๋“œ/๋Ÿฐํƒ€์ž„ ์ฆ‰์‹œ ์‹คํŒจ(์˜ˆ: ์—”ํ‹ฐํ‹ฐ ์†์„ฑ๋ช… ๋ถˆ์ผ์น˜, EF ๋งคํ•‘ ๋ˆ„๋ฝ). +- MED: ๋กœ๋” ์ธ์ ์…˜, config ํ•ซ๋ฆฌ๋กœ๋“œ ์‹œ ์ƒํƒœ ๋ˆ„์ˆ˜(์ปฌ๋Ÿผ ์‚ญ์ œ ํ›„ `_states` ์ž”์กด โ†’ ๊ฒฝ๋ฏธ). +- LOW: ํ•˜๋“œ์ฝ”๋”ฉ ์ƒ์ˆ˜, ๋ฏธ์‚ฌ์šฉ using. + +### STEP 8 โ€” ๋ณด๊ณ ์„œ ์–‘์‹ (๊ฐ๋…์ž๊ฐ€ ์ฑ„์›€) +``` +### [n]. [์ œ๋ชฉ] (HIGH/MED/LOW) +๋ฌธ์ œ: โ€ฆ +๊ทผ๊ฑฐ: ํŒŒ์ผ:์ค„ โ€” ์ฝ”๋“œ ์ธ์šฉ +์˜ํ–ฅ: โ€ฆ +์ˆ˜์ •: โ€ฆ +``` + +--- + +## 8. ๋‹จ์œ„ยทํ†ตํ•ฉ ๊ฒ€์ฆ ๊ณ„ํš (๊ฐ๋…์ž ์ง„๋‹จ ํ†ต๊ณผ ํ›„) + +### 8.1 ๋‹จ์œ„ํ…Œ์ŠคํŠธ (์ˆœ์ˆ˜ ๋ธ”๋กยท์—”์ง„) +| ๋Œ€์ƒ | ์ผ€์ด์Šค | +|:-----|:-------| +| `DeadTimeBuffer` | ฮธ=10s/ts=2s โ†’ 5์Šค์บ” ์ง€์—ฐ ์ •ํ™•, ฮธ **์„ฑ๊ฒฉ**: PhaseI(advisory ์—”์ง„)ยทPhaseII-UI(Tab 18)ยทPhaseIII(auto-write) ๋ฌธ์„œ์˜ **ยง6 ์ž”์—ฌ ๋ณด์ •ํ•ญ๋ชฉ(P-1~P-5)** ๊ณผ +> **์‹ ๊ทœ ์•ˆ์ „๊ธฐ๋Šฅ(์ „ํ™˜๋ฅ˜ ํ‰ํ˜•๋ณต๊ท€ ๋ชจ๋“œ)** ์˜ ํ„ดํ‚ค ๊ตฌํ˜„ ์ž‘์—…์ง€์‹œ์„œ. ๋‹ค๋ฅธ LLM์ด ์ฝ”๋“œ๋ฒ ์ด์Šค ์ถ”๊ฐ€ ํƒ์ƒ‰ ์—†์ด ๊ตฌํ˜„ํ•˜๋„๋ก +> **๊ฒ€์ฆ๋œ ํ˜„์žฌ ์ฝ”๋“œ ๊ธฐ์ค€์„ ** ์œ„์—์„œ ์ž‘์„ฑํ•œ๋‹ค. +> +> **๋ถˆ๋ณ€์‹(PhaseI ๊ณ„์Šน)**: P-1~P-5 ๋ฐ ์ „ํ™˜๋ฅ˜ ๋ชจ๋“œ์˜ **๊ถŒ์žฅ(advisory) ๊ณ„์‚ฐ๊นŒ์ง€๋Š” ์ œ์–ด ๋ ˆ์ง€์Šคํ„ฐ ์“ฐ๊ธฐ 0๊ฑด**. +> ์‹ค์ œ SP ์“ฐ๊ธฐ(์ „ํ™˜๋ฅ˜ ์ž๋™ ์‹คํ–‰ ํฌํ•จ)๋Š” **์ „๋ถ€ PhaseIII(WriteGuard)** ๊ฒฝ์œ . ๋ณธ ๋ฌธ์„œ๋Š” "๊ถŒ์žฅ๊ฐ’/๋ชจ๋“œ ์‚ฐ์ถœ + ํ‘œ์‹œ"๊นŒ์ง€๊ฐ€ ๋ฒ”์œ„. +> +> **์ž‘์—… ์ˆœ์„œ(์˜ํ–ฅ๋„ยท์˜์กด์„ฑ ๋ฐ˜์˜)**: ยงA ๋ฌธ์„œ๊ฐ๋ฆฌ ์„ ๋ฐ˜์˜ โ†’ **WO-1(P-5)** โ†’ **WO-2(P-2)** โ†’ **WO-3(P-1)** โ†’ +> **WO-4(P-4)** โ†’ **WO-5(P-3)** โ†’ **WO-6(์ „ํ™˜๋ฅ˜ ๋ณต๊ท€)** โ†’ ยงC ํ†ตํ•ฉ๊ฒ€์ฆ. P-7์€ ๊ธฐ์กด PhaseIII ๋ฌธ์„œ(ยงD ์ •์ • ๋ฉ”๋ชจ ์ฐธ์กฐ). + +--- + +## ยงA. ์„ ํ–‰ โ€” ๊ธฐ์กด ๋ฌธ์„œยท์ฝ”๋“œ ๊ฐ๋ฆฌ ๊ฒฐ๊ณผ (๋จผ์ € ๋ฐ˜์˜ํ•  ๊ฒƒ) โ˜… + +๋ณธ ์ž‘์—… ์ฐฉ์ˆ˜ ์ „ ์•„๋ž˜ ๋ฌธ์„œ ๋“œ๋ฆฌํ”„ํŠธ๋ฅผ ์ธ์ง€ํ•˜๊ณ  **ยงB ํ˜„์žฌ ์ฝ”๋“œ ๊ธฐ์ค€์„ ์„ ์ •๋ณธ**์œผ๋กœ ์‚ผ๋Š”๋‹ค. (๊ธฐ์กด PhaseI/II/III ๋ฌธ์„œ๋ฅผ ๊ธ€์ž๋Œ€๋กœ ๋”ฐ๋ฅด๋ฉด ๊นจ์ง€๋Š” ์ง€์ ๋“ค.) + +| # | ๋“ฑ๊ธ‰ | ์œ„์น˜ | ๋ฌธ์ œ | ์กฐ์น˜ | +|:-:|:----:|:-----|:-----|:-----| +| A1 | **HIGH** | PhaseI ยง5.4 DDLยทยง5.8 seedยทยง2 ๋ชจ๋ธ, PhaseII ยง2.2 | `ff_stream_config.level_tag`(TEXT) ์ปฌ๋Ÿผ์ด **์‹ค์ œ ์Šคํ‚ค๋งˆยท`StreamConfig.LevelTag`ยท`StreamAdvisory.LevelTag`ยทConfigStore SELECT(์ธ๋ฑ์Šค 14)ยทSaveColumn INSERTยทController `levelTag`** ์— ์ „๋ถ€ ์กด์žฌํ•˜๋‚˜ **๊ทธ ๋ฌธ์„œ๋“ค์—” ๋ˆ„๋ฝ** | ๋ณธ ๋ฌธ์„œ๋Š” **ํ˜„์žฌ ์ฝ”๋“œ ๊ธฐ์ค€์„ (ยงB)** ์„ ์ •๋ณธ์œผ๋กœ ์‚ผ๋Š”๋‹ค. ๊ธฐ์กด DDL ๋ฌธ์„œ๋ฅผ ๊ทธ๋Œ€๋กœ ๋”ฐ๋ฅด์ง€ ๋ง ๊ฒƒ | +| A2 | **WO-6 ์„ ๊ฒฐ** | `src/Web/Program.cs:124~128` | (์ค‘๋ณต ๋ฒ„๊ทธ ์•„๋‹˜ โ€” ํ˜„์žฌ `AddHostedService()` **๋‹จ์ผ ๋“ฑ๋ก**์œผ๋กœ ์ •์ƒ.) ๋‹จ, WO-6 ๋ณต๊ตฌ ์ปจํŠธ๋กค๋Ÿฌ๊ฐ€ `FeedforwardSupervisor`์— ์ฃผ์ž… ์ ‘๊ทผ(ColumnState ARM)ํ•˜๋ ค๋ฉด **singleton ๋…ธ์ถœ ํ•„์š”** | `AddHostedService()`(128) โ†’ **`AddSingleton()` + `AddHostedService(sp=>sp.GetRequiredService())`** 2์ค„๋กœ ๊ต์ฒด(๋‹จ์ผ ์ธ์Šคํ„ด์Šค๋ฅผ hosted+injectable๋กœ). **WO-6 ์ฐฉ์ˆ˜ ์‹œ์—๋งŒ** ๋ณ€๊ฒฝ, ๊ทธ์ „์—” ํ˜„ ์ƒํƒœ ์œ ์ง€ | +| A3 | **MED** | PhaseII ยง2.3 ๋ณธ๋ฌธ vs ยง8 | ยง2.3 ๋ณธ๋ฌธ์€ `IKbAuthService`/`IsAdminAsync`๋กœ config CRUD๋ฅผ ๋ง‰๋Š” ์ฝ”๋“œ๋ฅผ ๋ณด์—ฌ์ฃผ๋‚˜, ยง8๊ณผ **์‹ค์ œ ์ปจํŠธ๋กค๋Ÿฌ๋Š” ์ธ์ฆ ์ œ๊ฑฐ**(config CRUD ๊ณต๊ฐœ) | ๋ณธ ๋ฌธ์„œ๋Š” **์ธ์ฆ ์—†๋Š” ํ˜„์žฌ ์ปจํŠธ๋กค๋Ÿฌ(ยงB)** ๊ธฐ์ค€. auth ์žฌ๋„์ž…์€ PhaseIII(์“ฐ๊ธฐ ์‹œ)์—์„œ๋งŒ | +| A4 | LOW | PhaseI ยง2 | `StreamAdvisory` ๋ ˆ์ฝ”๋“œ์— `LevelTag` ์—†์Œ, enum์— `[JsonStringEnumConverter]` ํ‘œ๊ธฐ ์—†์Œ โ€” ์‹ค์ œ๋Š” ๋‘˜ ๋‹ค ์žˆ์Œ | ยงB ๊ธฐ์ค€์„  ์‚ฌ์šฉ | +| A5 | LOW | PhaseI ยง6 P-2 | "TempCorrection(ๅทฒ๊ตฌํ˜„)" โ€” `TempCorrection.PressureCompensated`๋Š” ์กด์žฌํ•˜๋‚˜ **์—”์ง„์—์„œ ํ˜ธ์ถœ๋˜์ง€ ์•Š๋Š” ์ฃฝ์€ ์ฝ”๋“œ** | WO-2์—์„œ ๋ฐฐ์„  | +| A6 | LOW | PhaseIII ยง6 | ๋ณ€๊ฒฝ๋Œ€์ƒ `src/Infrastructure/OpcUa/OpcUaClientService.cs`๋Š” **๊ทธ ์ด๋ฆ„์˜ ํŒŒ์ผ์ด ์—†์Œ**. ๋‹จ, **`src/Infrastructure/OpcUa/ExperionOpcWriteClient.cs`(namespace `...Infrastructure.Control`)๊ฐ€ ์ด๋ฏธ ์กด์žฌ**(์“ฐ๊ธฐ ํด๋ผ์ด์–ธํŠธ โ€” ๊ฐ€๋“œ ์—†์Œ). NodeId `ns=3;s="{tag}.sp"` ๊ทœ์น™ ๋ฏธ๊ฒ€์ฆ | ยงD ์ฐธ์กฐ โ€” PhaseIII๋Š” **๊ธฐ์กด `ExperionOpcWriteClient` ์žฌ์‚ฌ์šฉ/ํ™•์žฅ**(์‹ ๊ทœ ํŒŒ์ผ X). NodeId๋Š” ์„œ๋ฒ„ ๋ธŒ๋ผ์šฐ์ฆˆ๋กœ ํ™•์ • | + +> **advisory ์“ฐ๊ธฐ ๋ถˆ๋ณ€์‹์˜ ์ •ํ™•ํ•œ ๋ฒ”์œ„**: ์ฝ”๋“œ๋ฒ ์ด์Šค ์ „์ฒด์—” ๋ฒ”์šฉ `ExperionOpcWriteClient`(๋‹ค๋ฅธ ๊ธฐ๋Šฅ์šฉ)๊ฐ€ **์กด์žฌ**ํ•œ๋‹ค. +> ๋ถˆ๋ณ€์‹์€ **"FF advisory ๊ฒฝ๋กœ๊ฐ€ ๊ทธ ์“ฐ๊ธฐ ํด๋ผ์ด์–ธํŠธ๋ฅผ ํ˜ธ์ถœํ•˜์ง€ ์•Š๋Š”๋‹ค"** ๋Š” ์˜๋ฏธ. +> ๊ฒ€์ฆ grep์€ **FF ํŒŒ์ผ์— ํ•œ์ •**: `grep -rE "ExperionOpcWriteClient|Write.*Async" src/Infrastructure/Control src/Web/Controllers/FeedforwardController.cs` โ†’ 0๊ฑด(ํ˜„์žฌ ์œ ์ง€๋จ). + +--- + +## ยงB. ๊ฒ€์ฆ๋œ ํ˜„์žฌ ์ฝ”๋“œ ๊ธฐ์ค€์„  (์ •๋ณธ โ€” 2026-05-31 ์‹ค๋…) + +๋‹ค๋ฅธ LLM์€ ์•„๋ž˜๋ฅผ **์‚ฌ์‹ค๋กœ ๊ฐ„์ฃผ**ํ•˜๊ณ , ๊ธฐ์กด PhaseI/II/III ๋ฌธ์„œ์™€ ์ถฉ๋Œ ์‹œ **๋ณธ ยงB๋ฅผ ์šฐ์„ **ํ•œ๋‹ค. + +### B.1 ๋ชจ๋ธ (`src/Core/Application/Feedforward/FeedforwardModels.cs`) +- `enum StreamRole { Commanded, LevelDriven, Monitor }` ยท `enum Confidence { A, B, C }` โ€” ๋‘˜ ๋‹ค `[JsonConverter(typeof(JsonStringEnumConverter))]`. +- `StreamConfig`: Key, FlowTag, Role, **LevelTag(string?)**, TargetCoeff, ThetaUpSec, ThetaDnSec, TauSec, SpMin, SpMax, RateUpPerMin, RateDnPerMin, RefluxFromProduct, Grade. +- `ColumnConfig`: Id, Name, Enabled, AdvisoryOnly(=true ๊ฐ•์ œ), FeedTag, PressureTag, LevelTags, ScanSec, FeedFilterTauSec, FeedMoveThresholdPerMin, PressFilterTauSec, PressureBand, SettleSec, StaleSec, ProductKey, Streams. +- `TagSample(Tag, Value, Good, Timestamp)`. +- `PvSnapshot(Feed, Pressure?, Levels[], Streams{keyโ†’TagSample})`. +- `StreamAdvisory(Key, FlowTag, Role, Pv, RecommendedSp?, Gap?, Trend, Valid, Grade, **LevelTag?**, Note)`. +- `AdvisoryResult(ColumnId, ColumnName, ComputedAt, Enabled, Transient, TransientReason, FeedFiltered, Streams[], VLoss?, Yield?, MassBalanceState)`. + +### B.2 ์—ฐ์‚ฐ๋ธ”๋ก (`src/Infrastructure/Control/ComputationBlocks.cs`) +`Num`(Clamp/IsFinite), `FirstOrderLag`(Seed/Step), `MovingAverage(windowSamples)`(Push), `DeadTimeBuffer`(Through โ€” ์ฆ๊ฐ€์ „์šฉ ๋งยท๋น„๋Œ€์นญ ฮธ ๋ณด์กด), `RateLimiter`(Seed/Step ๋น„๋Œ€์นญ), `Derivative`(Update), `TempCorrection.PressureCompensated`(**๋ฏธ๋ฐฐ์„  ์ฃฝ์€ ์ฝ”๋“œ**). + +### B.3 ์—”์ง„ (`src/Infrastructure/Control/FeedforwardEngine.cs`) +- `StreamState{ Dead, Lag, Rate, LastRec }`, `ColumnState{ FeedFilter, PressFilter, FeedDeriv, SettleTimerSec, Initialized, Streams{keyโ†’StreamState} }`. +- `FeedforwardEngine.Tick(ColumnConfig cfg, PvSnapshot pv, ColumnState st, DateTime now) โ†’ AdvisoryResult` โ€” **์ˆœ์ˆ˜ํ•จ์ˆ˜(I/O ์—†์Œ)**. +- ํ๋ฆ„: FEED ํ’ˆ์งˆ๊ฒŒ์ดํŠธโ†’Hold / FEED ํ•„ํ„ฐ(EMA) / ์‹œ๋“œ / ๊ณผ๋„ยท์••๋ ฅ ๊ฒŒ์ดํŠธ(transient) / ์ŠคํŠธ๋ฆผ 2-pass(commandedโ†’reflux) / ๋ฌผ์งˆ์ˆ˜์ง€(transient ์•„๋‹ ๋•Œ `vloss=ff-(D+P+B)`, `yield=100*P/ff`). +- ์—ญํ• ๋ณ„: Commanded=๋น„๋Œ€์นญฮธ DeadTimeโ†’Lagโ†’Kโ†’RateLimit / LevelDriven=`K*ff`(์ฆ‰์‹œ) / Monitor=null. + +### B.4 ์ˆ˜๊ธ‰/์ €์žฅ/์ปจํŠธ๋กค๋Ÿฌ/DI +- `FeedforwardSupervisor`(BackgroundService): ์Šค์ฝ”ํ”„๋งˆ๋‹ค `IFeedforwardConfigStore.LoadAllAsync` + `IExperionDbService.GetRealtimeRecordsByTagNamesAsync`๋กœ `PvSnapshot` ๊ตฌ์„ฑ โ†’ `Tick` โ†’ `IFeedforwardAdvisoryStore.Set`. ์“ฐ๊ธฐ ์—†์Œ. ์ปฌ๋Ÿผ๋ณ„ `ColumnState` ๋‹จ์ผ ๋ฃจํ”„ ์†Œ์œ (๋ฝ ์—†์Œ). +- `IFeedforwardConfigStore{ LoadAllAsync, SaveColumnAsync, DeleteColumnAsync }`, `IFeedforwardAdvisoryStore{ Set, Get, GetAll }`. +- `FeedforwardConfigStore`: ADO(EF ์ปค๋„ฅ์…˜). LoadAll์€ ์ปฌ๋Ÿผ+์ŠคํŠธ๋ฆผ 2์ฟผ๋ฆฌ(์ŠคํŠธ๋ฆผ SELECT์— `level_tag` ์ธ๋ฑ์Šค 14 ํฌํ•จ). Save/Delete๋Š” **ํŒŒ๋ผ๋ฏธํ„ฐํ™” `P()` ํ—ฌํผ + ํŠธ๋žœ์žญ์…˜**. +- `FeedforwardController` (`api/ff`): `GET/POST/DELETE config`(**์ธ์ฆ ์—†์Œ**), `GET advisory`ยท`advisory/{id}`(๊ณต๊ฐœ). `MapColumn`/`MapConfig`๋กœ camelCase ๋ช…์‹œ. +- DI(Program.cs:124~128): Engine=Singleton, AdvisoryStore=Singleton, ConfigStore=Scoped, Supervisor=**๋‹จ์ผ AddHostedService**(์ค‘๋ณต ์—†์Œ). WO-6์—์„œ singleton+hosted๋กœ ๊ต์ฒด(A2). +- DDL(ExperionDbContext.InitializeAsync, line ~1066~1103): `ff_column_config`, `ff_stream_config`(grade, **level_tag** ํฌํ•จ)์„ **ํ•˜๋‚˜์˜ `ExecuteSqlRawAsync`์— ์—ฌ๋Ÿฌ ๋ฌธ์žฅ(CREATE;CREATE;ALTER)** ์œผ๋กœ ๋ฉฑ๋“ฑ ์ƒ์„ฑ(Npgsql๋Š” ๋‹ค๋ฌธ์žฅ ํ—ˆ์šฉ โ€” PhaseI์˜ "ํ•œ ํ˜ธ์ถœ ํ•œ ๋ฌธ์žฅ" ์ฃผ์˜๋Š” ํ˜„์žฌ ์ฝ”๋“œ์—์„  ๋ฌดํšจ). try/catch๋กœ "[ExperionDb] ์ดˆ๊ธฐํ™” ์‹คํŒจ" ๊ฒฝ๊ณ  ํ›„ `return false`. + +### B.5 C-6111 ํƒœ๊ทธ ๋งคํ•‘ (๋ธŒ๋ ˆ์ธ์Šคํ† ๋ฐ ํ™•์ •) +| ํ‚ค | ํƒœ๊ทธ | ์˜๋ฏธ | ์—ญํ• (seed) | ์ „ํ™˜๋ฅ˜ ์‹œ | +|:--:|:-----|:-----|:----------|:----------| +| FEED | ficq-6101 | ์›๋ฃŒ ํˆฌ์ž…(๋ฌผ์งˆ์ˆ˜์ง€ ๊ธฐ์ค€) | (์ž…๋ ฅยท์™ธ๋ž€) | **์ฐจ๋‹จ(โ†’0)** | +| R | ficq-6113 | ํ™˜๋ฅ˜(reflux) | Commanded(RefluxFromProduct) | **์ตœ๋Œ€(์ „๋Ÿ‰ ํ™˜๋ฅ˜)** | +| P | ficq-6118 | ์ธก๋ฅ˜ ์ œํ’ˆ(PGMEA) | Commanded | **์ฐจ๋‹จ(โ†’0)** | +| D | ficq-6114 | ํƒ‘์ • ๊ฒฝ๋น„๋ฌผ(์ €๋น„์ ) ๋ฐฐ์ถœ | LevelDriven | **์ฐจ๋‹จ(โ†’0)** | +| B | ficq-6116 | ํƒ‘์ € ์ค‘๋น„๋ฌผ(๊ณ ๋น„์ ) ๋ฐฐ์ถœ | LevelDriven | **์ฐจ๋‹จ(โ†’0)** | +| ๋ณด์กฐ | tica-6111a(ํƒ‘์ €/๋ฆฌ๋ณด์ผ๋Ÿฌ), pica-6111ยทpi-6111(์ง„๊ณต ~48.5torr), lica-6113/li-6111(๋ ˆ๋ฒจ), ti-6111b/c/d(ํ”„๋กœํŒŒ์ผ) | | | + +> ์ถœ์ฒ˜: D/P/B ๋งคํ•‘ = `knowledge/PGMEA_์ธก๋ฅ˜์ถ”์ถœ์šด์ „๋ฐฉ์‹_์ฃผ์˜์ .md ยง1` + PhaseI ยง5.8 seed. ํƒœ๊ทธ ๊ณ„์ธกํ˜„ํ™ฉ = `docs/๋ณด์กฐ์šด์ „-๋ธŒ๋ ˆ์ธ์Šคํ† ๋ฐ.md ยง10.2`(ti-6111a.pv=0 ๊ณ ์žฅ์˜์‹ฌ ์ฃผ์˜). +> **์šด์ „ ์ •์„(`PGMEA_์ธก๋ฅ˜์ถ”์ถœ์šด์ „๋ฐฉ์‹_์ฃผ์˜์ .md ยง4.2ยทยง4.3`)**: ์ธก๋ฅ˜ ์กฐ์„ฑ์ด ๋ชฉํ‘œ ์ดํƒˆ/์™ธ๋ž€ ์‹œ โ†’ โ‘  **์ธก๋ฅ˜๋ฅผ ๋จผ์ € ์ค„์ด๊ฑฐ๋‚˜ ์ผ์‹œ ์ค‘๋‹จ**(์˜ค์—ผ ์ œํ’ˆ ๋ฐฉ์ง€) โ†’ โ‘ก **ํ™˜๋ฅ˜๋น„๋ฅผ ๋†’์—ฌ ํƒ‘ ๋‚ด๋ถ€ ์žฌ์•ˆ์ •ํ™”** โ†’ โ‘ข ํšŒ๋ณต๋˜๋ฉด ์ธก๋ฅ˜ ์žฌ๊ฐœ. "ํƒ‘์„ ์•ˆ์ •์‹œํ‚จ ํ›„ ๋ฝ‘๋Š”๋‹ค"๊ฐ€ ์›์น™. **์ „ํ™˜๋ฅ˜ ๋ชจ๋“œ(WO-6)๋Š” ์ด ยง4.3 ์ •์„์˜ ๊ทน๋‹จ(๋“œ๋กœ์šฐ ์ „๋ฉด ์ค‘๋‹จยท์ „๋Ÿ‰ ํ™˜๋ฅ˜)์„ ์ƒํƒœ๊ธฐ๊ณ„๋กœ ํ˜•์‹ํ™”ํ•œ ๊ฒƒ.** + +--- + +## ยง0. ๋ชจ๋ธ ๊ณตํ†ต ํ™•์žฅ (๋ชจ๋“  WO ์„ ํ–‰) โ€” `FeedforwardModels.cs` + +WO๋“ค์ด ๊ณต์œ ํ•˜๋Š” ํ•„๋“œ๋ฅผ **ํ•œ ๋ฒˆ์—** ์ถ”๊ฐ€ํ•œ๋‹ค(๋ ˆ์ฝ”๋“œ๋ผ `with` ํ˜ธํ™˜). **camelCase ์ง๋ ฌํ™”๋Š” ์ปจํŠธ๋กค๋Ÿฌ Map์—์„œ ๋ช…์‹œ** (PhaseI ๊ทœ์น™). + +```csharp +// enum ์ถ”๊ฐ€ +[JsonConverter(typeof(JsonStringEnumConverter))] +public enum ColumnMode { Normal, Recovering, Returning } // WO-6 ์ „ํ™˜๋ฅ˜ ์ƒํƒœ๊ธฐ๊ณ„ + +// StreamConfig ์ถ”๊ฐ€ ํ•„๋“œ (DDL ff_stream_config ๋™๋ฐ˜) +// P-1(ฮธ์ œ์•ˆ)ยท์ „ํ™˜๋ฅ˜ reflux ์‹๋ณ„์šฉ +public bool IsReflux { get; init; } // ์ „ํ™˜๋ฅ˜ ์‹œ "์ „๋Ÿ‰ ํ™˜๋ฅ˜" ๋Œ€์ƒ. (์—†์œผ๋ฉด RefluxFromProduct๋กœ ๋Œ€์ฒด์‹๋ณ„) +public double RecoverySp { get; init; } = double.NaN; // ์ „ํ™˜๋ฅ˜ ์ค‘ ์ด ์ŠคํŠธ๋ฆผ ๊ถŒ์žฅSP. NaN์ด๋ฉด ๊ทœ์น™๊ธฐ๋ณธ(draw=0, reflux=SpMax) + +// ColumnConfig ์ถ”๊ฐ€ ํ•„๋“œ (DDL ff_column_config ๋™๋ฐ˜) +// P-2(PCT/์ฐจ์˜จ) +public IReadOnlyList TempTags { get; init; } = Array.Empty(); // ํ”„๋กœํŒŒ์ผ ์˜จ๋„ base tag (์ƒโ†’ํ•˜ ์ˆœ์„œ) +public string? SensitiveTrayTag { get; init; } // ๋ฏผ๊ฐํŠธ๋ ˆ์ด(ํ”„๋ก ํŠธ ์ง€ํ‘œ). null์ด๋ฉด ฮ”T(์ƒ-ํ•˜)๋กœ ๋Œ€์ฒด +public double DTdP { get; init; } = 0.0; // dT/dP [ยฐC/์••๋ ฅ๋‹จ์œ„]. 0์ด๋ฉด PCT ๋ฏธ์ ์šฉ(์ƒ์˜จ๋„) +public double PRef { get; init; } = double.NaN; // ์••๋ ฅ ๊ธฐ์ค€์ . NaN์ด๋ฉด ์ฒซ ์ •์ƒ์••๋ ฅ ์‹œ๋“œ +// P-1(ฮธ ์ž๋™ํŠœ๋‹) +public string? SteamOpTag { get; init; } // TICA-6111A.OP(์ŠคํŒ€) โ€” ๋ถ€๋ถ„์ƒ๊ด€ 2๋ฒˆ์งธ ์ž…๋ ฅ(ํ๋ฃจํ”„ ์˜ค์—ผ ํšŒํ”ผ) +public bool ThetaAutoTune { get; init; } // ฮธ ์‹๋ณ„ ๊ฐ€๋™ ์—ฌ๋ถ€(์ œ์•ˆ๋งŒ, ์ž๋™๋ฐ˜์˜ X) +// P-4(๋А๋ฆฐ ๋ฐ”์ด์–ด์Šค) +public double BiasMaWindowSec { get; init; } = 6*3600; // K_obs/k_V ์žฅ๊ธฐ MA ์ฐฝ(๊ธฐ๋ณธ 6h) +// WO-6(์ „ํ™˜๋ฅ˜ ๋ณต๊ท€) +public bool RecoveryEnabled { get; init; } // ์ „ํ™˜๋ฅ˜ ๊ถŒ์žฅ ๊ธฐ๋Šฅ on/off +public bool RecoveryAutoArm { get; init; } // true=์ž๋™๊ถŒ์žฅ, false=์šด์ „์› 1ํด๋ฆญ ๋ฌด์žฅ ํ›„์—๋งŒ +public double ImbalanceTriggerFrac { get; init; } = 0.10; // |V_loss_MA|/F ์ง€์† ์ดˆ๊ณผ ์‹œ ํŠธ๋ฆฌ๊ฑฐ(๊ธฐ๋ณธ 10%) +public double ImbalanceTriggerSec { get; init; } = 600; // ์ง€์† ์‹œ๊ฐ„(๊ธฐ๋ณธ 10๋ถ„) +public double RecoverySettleSec { get; init; } = 1800; // ์ „ํ™˜๋ฅ˜ ํ‰ํ˜• dwell(๊ธฐ๋ณธ 30๋ถ„) +public double ReturnRampSec { get; init; } = 600; // ๋ณต๊ท€ ์‹œ draw/feed ๋žจํ”„(๊ธฐ๋ณธ 10๋ถ„) +public double FeedRecoverySp { get; init; } = 0.0; // ์ „ํ™˜๋ฅ˜ ์ค‘ FEED ๊ถŒ์žฅ๊ฐ’(๊ธฐ๋ณธ 0=์ฐจ๋‹จ) +public string? DeltaPTag { get; init; } // ํƒ‘ ์ฐจ์••(ฮ”P) โ€” ํ”Œ๋Ÿฌ๋”ฉ/๋น„์‚ฐ ํŠธ๋ฆฌ๊ฑฐ(์ฃผ์˜์ ยง3 4์ˆœ์œ„). null=๋ฏธ์‚ฌ์šฉ +public double DeltaPFloodLimit { get; init; } = double.MaxValue; // ฮ”P ์ƒํ•œ(์ดˆ๊ณผ ์ง€์† ์‹œ ํŠธ๋ฆฌ๊ฑฐ) + +// StreamAdvisory ์ถ”๊ฐ€ ํ•„๋“œ +public string? GradeReason { get; init; } // P-5 ๊ฐ•๋“ฑ ์‚ฌ์œ  +public double? ThetaSuggestUpSec { get; init; } // P-1 ์ œ์•ˆ ฮธโ†‘ (null=์‹ ๋ขฐ๋ถ€์กฑ) +public double? ThetaSuggestDnSec { get; init; } // P-1 ์ œ์•ˆ ฮธโ†“ +public double? ThetaSuggestConf { get; init; } // P-1 ์ƒ๊ด€ ์‹ ๋ขฐ 0~1 +public double? KObsSuggest { get; init; } // P-4 ๊ด€์ธก K ์žฅ๊ธฐ์ถ”์„ธ ์ œ์•ˆ + +// AdvisoryResult ์ถ”๊ฐ€ ํ•„๋“œ +public ColumnMode Mode { get; init; } = ColumnMode.Normal; // WO-6 +public string? ModeReason { get; init; } +public double? VLossMa { get; init; } // P-4/WO-6 ์žฅ๊ธฐ MA V_loss +public IReadOnlyList? Temps { get; init; } // P-2 PCT/์ฐจ์˜จ ๋ชจ๋‹ˆํ„ฐ +public string? FrontPositionState { get; init; } // P-3 +public string? FrontTrimAdvice { get; init; } // P-3 + +public sealed record TempPoint(string Tag, double Raw, double Pct, bool Good); +``` + +> **๋ ˆ์ฝ”๋“œ ํ™•์žฅ ์ฃผ์˜**: `StreamAdvisory`ยท`AdvisoryResult`๋Š” **์œ„์น˜ ์ธ์ž(positional) record** ๋‹ค. ์ƒˆ ํ•„๋“œ๋Š” **positional ํŒŒ๋ผ๋ฏธํ„ฐ๋กœ ์ถ”๊ฐ€ํ•˜๋ฉด ๊ธฐ์กด `new StreamAdvisory(...)` ํ˜ธ์ถœ์ด ์ „๋ถ€ ๊นจ์ง„๋‹ค.** โ†’ **์ƒˆ ํ•„๋“œ๋Š” ์œ„์™€ ๊ฐ™์ด `{ get; init; }` ๋ณธ๋ฌธ ํ”„๋กœํผํ‹ฐ๋กœ ์ถ”๊ฐ€**(positional ์ƒ์„ฑ์ž ๋ถˆ๋ณ€)ํ•˜๊ณ , ์ƒ์„ฑ๋ถ€์—์„œ `with { ... }` ๋˜๋Š” object initializer๋กœ ์ฑ„์šด๋‹ค. ์—”์ง„์˜ ๊ธฐ์กด `new StreamAdvisory(...)`/`new AdvisoryResult(...)` ํ˜ธ์ถœ์€ ๊ทธ๋Œ€๋กœ ๋‘๊ณ  ๋’ค์— `with { GradeReason = ..., Mode = ... }`๋ฅผ ๋ถ™์ธ๋‹ค. + +**DDL ๋ธํƒ€** (ExperionDbContext.InitializeAsync, line ~1102 ๊ธฐ์กด ff ๋ธ”๋ก ๋ โ€” ๊ฐ™์€ `ExecuteSqlRawAsync` ๋‹ค๋ฌธ์žฅ ๋ธ”๋ก์— ๋ฉฑ๋“ฑ ALTER ์ถ”๊ฐ€; Npgsql ๋‹ค๋ฌธ์žฅ ํ—ˆ์šฉ. ์œ„์น˜๋Š” `ALTER TABLE ff_stream_config ADD COLUMN IF NOT EXISTS level_tag TEXT;` ๋ฐ”๋กœ ๋’ค): +```sql +ALTER TABLE ff_stream_config ADD COLUMN IF NOT EXISTS is_reflux BOOLEAN NOT NULL DEFAULT FALSE; +ALTER TABLE ff_stream_config ADD COLUMN IF NOT EXISTS recovery_sp DOUBLE PRECISION; -- NULL=๊ทœ์น™๊ธฐ๋ณธ +ALTER TABLE ff_column_config ADD COLUMN IF NOT EXISTS temp_tags TEXT; -- ์ฝค๋งˆ๊ตฌ๋ถ„ +ALTER TABLE ff_column_config ADD COLUMN IF NOT EXISTS sensitive_tray_tag TEXT; +ALTER TABLE ff_column_config ADD COLUMN IF NOT EXISTS dtdp DOUBLE PRECISION NOT NULL DEFAULT 0; +ALTER TABLE ff_column_config ADD COLUMN IF NOT EXISTS p_ref DOUBLE PRECISION; -- NULL=์‹œ๋“œ +ALTER TABLE ff_column_config ADD COLUMN IF NOT EXISTS steam_op_tag TEXT; +ALTER TABLE ff_column_config ADD COLUMN IF NOT EXISTS theta_auto_tune BOOLEAN NOT NULL DEFAULT FALSE; +ALTER TABLE ff_column_config ADD COLUMN IF NOT EXISTS bias_ma_window_sec DOUBLE PRECISION NOT NULL DEFAULT 21600; +ALTER TABLE ff_column_config ADD COLUMN IF NOT EXISTS recovery_enabled BOOLEAN NOT NULL DEFAULT FALSE; +ALTER TABLE ff_column_config ADD COLUMN IF NOT EXISTS recovery_auto_arm BOOLEAN NOT NULL DEFAULT FALSE; +ALTER TABLE ff_column_config ADD COLUMN IF NOT EXISTS imbalance_trigger_frac DOUBLE PRECISION NOT NULL DEFAULT 0.10; +ALTER TABLE ff_column_config ADD COLUMN IF NOT EXISTS imbalance_trigger_sec DOUBLE PRECISION NOT NULL DEFAULT 600; +ALTER TABLE ff_column_config ADD COLUMN IF NOT EXISTS recovery_settle_sec DOUBLE PRECISION NOT NULL DEFAULT 1800; +ALTER TABLE ff_column_config ADD COLUMN IF NOT EXISTS return_ramp_sec DOUBLE PRECISION NOT NULL DEFAULT 600; +ALTER TABLE ff_column_config ADD COLUMN IF NOT EXISTS feed_recovery_sp DOUBLE PRECISION NOT NULL DEFAULT 0; +ALTER TABLE ff_column_config ADD COLUMN IF NOT EXISTS delta_p_tag TEXT; +ALTER TABLE ff_column_config ADD COLUMN IF NOT EXISTS delta_p_flood_limit DOUBLE PRECISION NOT NULL DEFAULT 1e9; +``` +> **ConfigStore ๋™๋ฐ˜ ์ˆ˜์ •**: `LoadAllAsync`์˜ ๋‘ SELECT์— ์‹ ๊ทœ ์ปฌ๋Ÿผ ์ถ”๊ฐ€(์ฝ๊ธฐ ์ธ๋ฑ์Šค ์‹œํ”„ํŠธ ์ฃผ์˜ โ€” ์ƒˆ ์ปฌ๋Ÿผ์€ **ํ•ญ์ƒ SELECT ๋์— append**ํ•˜๊ณ  ์ƒˆ ์ธ๋ฑ์Šค๋กœ ์ฝ์„ ๊ฒƒ), `SaveColumnAsync`์˜ INSERT/UPDATE์— ์‹ ๊ทœ ํŒŒ๋ผ๋ฏธํ„ฐ ์ถ”๊ฐ€, `MapConfig`์— camelCase ํ•„๋“œ ์ถ”๊ฐ€. **์ธ๋ฑ์Šค ์‹œํ”„ํŠธ๊ฐ€ PhaseI ์ง„๋‹จ์˜ ๋‹จ๊ณจ ๋ฒ„๊ทธ**์ด๋ฏ€๋กœ SELECT ์ปฌ๋Ÿผ ์ˆœ์„œ์™€ `rd.GetXxx(n)` ๋ฒˆํ˜ธ๋ฅผ 1:1 ๋Œ€์กฐ ๊ฒ€์ฆ. + +--- + +## WO-1 โ€” P-5 confidence ์ž๋™๊ฐ•๋“ฑ (1์ˆœ์œ„, ๋…ธ๋ ฅ ๅฐ) + +**๋ชฉ์ **: config์˜ ์ •์  `Grade`(A/B/C)๋ฅผ **์‹ค์‹œ๊ฐ„ ์ž…๋ ฅ ๊ฑด์ „์„ฑ์œผ๋กœ ๊ฐ•๋“ฑ**ํ•ด, ์šด์ „์›์ด "์ง€๊ธˆ ์ด ๊ถŒ์žฅ๊ฐ’์„ ๋ฏฟ์„์ง€"๋ฅผ ์ƒ‰์œผ๋กœ ์•ˆ๋‹ค. PhaseIII auto-write์˜ **์•ˆ์ „ ๊ฒŒ์ดํŠธ ์ „์ œ**(Grade A๋งŒ ์“ฐ๊ธฐ). +**๊ทผ๊ฑฐ**: spec ยง14.3(๋ณด์ • 3๋“ฑ๊ธ‰), ยง14.5(์‹ ๋ขฐ๋„ ํ”Œ๋ž˜๊ทธ). PhaseII ยง6 ํ›…("`StreamAdvisory.Grade` โ† P-5 ์—ฐ๊ฒฐ์ "). + +**์„ค๊ณ„**: config `Grade`๋Š” **์ƒํ•œ(best-case)**. ์—”์ง„์ด ์•„๋ž˜ ์‚ฌ์œ ๋กœ ํ•œ ๋‹จ๊ณ„์”ฉ ๊ฐ•๋“ฑํ•˜๊ณ  `GradeReason`์— ๊ธฐ๋ก. + +| ๊ฐ•๋“ฑ ์‚ฌ์œ  | ์ ์šฉ | +|:----------|:-----| +| PV stale/BAD(์‹ ์„ ๋„ ์ดˆ๊ณผ) | ํ•ด๋‹น ์ŠคํŠธ๋ฆผ โ†’ ์ตœ์†Œ B, ์—ฐ์† ์‹œ C | +| ๊ณผ๋„(transient) ์ค‘ | โ†’ ํ•œ ๋‹จ๊ณ„ ๊ฐ•๋“ฑ(์ •์ฐฉ ์ „ ์‹ ๋ขฐ ๋‚ฎ์Œ) | +| ์••๋ ฅ ๋ถˆ์•ˆ์ •(pUnstable) | ์ปฌ๋Ÿผ ์ „์ฒด โ†’ ํ•œ ๋‹จ๊ณ„ ๊ฐ•๋“ฑ | +| analyzer/์˜จ๋„์ง€ํ‘œ ๋ถ€์žฌ์ธ๋ฐ ๋“ฑ๊ธ‰์ด C ํ•ญ๋ชฉ ์˜์กด(P-3 ์—ฐ๊ณ„) | C ์œ ์ง€ | +| ๋ฌผ์งˆ์ˆ˜์ง€ ๋ถˆ์ผ์น˜(`mbState`โ‰ "์ •์ƒ") | commanded ์ŠคํŠธ๋ฆผ โ†’ ํ•œ ๋‹จ๊ณ„ ๊ฐ•๋“ฑ | + +**๊ตฌํ˜„ (`FeedforwardEngine.cs`)** โ€” ์ˆœ์ˆ˜ํ•จ์ˆ˜ ์œ ์ง€. ํ—ฌํผ ์ถ”๊ฐ€: +```csharp +private static (Confidence g, string? why) Downgrade(Confidence baseG, params (bool hit,string why)[] rules) +{ + int lvl = (int)baseG; // A=0,B=1,C=2 + string? why = null; + foreach (var (hit, w) in rules) if (hit) { lvl = Math.Min(2, lvl + 1); why = why is null ? w : why + "; " + w; } + return ((Confidence)lvl, why); +} +``` +`BuildAdvisory`์—์„œ ์ŠคํŠธ๋ฆผ๋ณ„ ์ ์šฉ(`pv.Streams[key].Good` ์‹ ์„ ๋„, `transient`, mbState ๋ถˆ์ผ์น˜ ํ”Œ๋ž˜๊ทธ๋ฅผ ์ธ์ž๋กœ ๋ฐ›๊ฒŒ ์‹œ๊ทธ๋‹ˆ์ฒ˜ ํ™•์žฅ), ๊ฒฐ๊ณผ๋ฅผ `with { Grade = g, GradeReason = why }`. ์ปฌ๋Ÿผ ๊ณตํ†ต ์‚ฌ์œ (pUnstable)๋Š” Tick์—์„œ ์ผ๊ด„ ํ•œ ๋‹จ๊ณ„ ์ถ”๊ฐ€ ๊ฐ•๋“ฑ. + +**UI(ff.js, ๊ธฐ์กด P-6 ์ž์‚ฐ)**: ์ด๋ฏธ `.ff-grade-A/B/C` ์ƒ‰ ํด๋ž˜์Šค ์กด์žฌ โ†’ `s.gradeReason`์„ ์…€ `title`(ํˆดํŒ)๋กœ ๋…ธ์ถœ๋งŒ ์ถ”๊ฐ€. + +**ํ…Œ์ŠคํŠธ(xUnit, PhaseI ยง5.7 ํ”„๋กœ์ ํŠธ)**: +- ์‹ ์„  PV + ์ •์ƒ โ†’ config Grade ๊ทธ๋Œ€๋กœ. +- stale PV โ†’ โ‰ฅB. transient โ†’ ํ•œ ๋‹จ๊ณ„. stale+transient+mb๋ถˆ์ผ์น˜ โ†’ C(๋ฐ”๋‹ฅ). +- `Downgrade`๋Š” A๋ฅผ ๋„˜์–ด C์—์„œ ๋” ๋‚ด๋ ค๊ฐ€์ง€ ์•Š์Œ(ํด๋žจํ”„). + +**๊ฒ€์ฆ**: ๋นŒ๋“œ 0/0, `grep`๋กœ ์“ฐ๊ธฐ 0๊ฑด ์œ ์ง€, advisory ์‘๋‹ต์— `gradeReason` ๋“ฑ์žฅ, ํ•œ ์ŠคํŠธ๋ฆผ ๊ฐ•์ œ stale ์‹œ ํ•ด๋‹น ์นด๋“œ๋งŒ ๊ฐ•๋“ฑ. + +--- + +## WO-2 โ€” P-2 PCT/์ฐจ์˜จ ๋ชจ๋‹ˆํ„ฐ (2์ˆœ์œ„, ๋ฐ˜์ฏค ์™„์„ฑ) + +**๋ชฉ์ **: ์ฃฝ์€ ์ฝ”๋“œ `TempCorrection.PressureCompensated`๋ฅผ **์—”์ง„์— ๋ฐฐ์„ ** + `DiffTemp` ์ถ”๊ฐ€ โ†’ ์ง„๊ณต๋…ธ์ด์ฆˆ ์ œ๊ฑฐ๋œ PCTยท์ฐจ์˜จ์„ ๋ชจ๋‹ˆํ„ฐ๋กœ ์‚ฐ์ถœ. **P-3(ํ”„๋ก ํŠธ ์œ„์น˜)ยทP-5(C๋“ฑ๊ธ‰ ๊ทผ๊ฑฐ)์˜ ์ž…๋ ฅ**. +**๊ทผ๊ฑฐ**: spec ยง13.3(PCT/ฮ”T), ยง13.6(๋ธ”๋ก), ยง14.1(dT/dPโ‰ˆ0.5ยฐC/torr โ€” ์ง„๊ณต ยฑ2torr๊ฐ€ ๊ตฌ๋ฐฐ ์ ˆ๋ฐ˜ โ†’ PCT ํ•„์ˆ˜). + +**์‹ ๊ทœ ๋ธ”๋ก (`ComputationBlocks.cs`)**: +```csharp +/// ์ฐจ์˜จ/์ด์ค‘์ฐจ์˜จ โ€” ๊ณตํ†ต๋ชจ๋“œ(์••๋ ฅยท๊ณ„์ ˆ) ์ƒ์‡„, ํ”„๋ก ํŠธ ์ด๋™๋งŒ ๋ถ€๊ฐ. ยง13.3 +public static class DiffTemp +{ + public static double Delta(double tHi, double tLo) => tHi - tLo; // ๋‘ ํŠธ๋ ˆ์ด ์ฐจ์˜จ + public static double Double(double tA, double tB, double tC) => (tA - tB) - (tB - tC); // ์ด์ค‘์ฐจ์˜จ(๊ณก๋ฅ ) +} +``` +> `TempCorrection.PressureCompensated`๋Š” ๊ทธ๋Œ€๋กœ ์‚ฌ์šฉ(์ถ”๊ฐ€ ์ฝ”๋“œ ์—†์Œ). PCT = `T_meas โˆ’ dTdPยท(P โˆ’ P_ref)`. + +**์—”์ง„ ๋ฐฐ์„ **: +- `ColumnState`์— `FirstOrderLag PRefSeed`(๋˜๋Š” ๋‹จ์ˆœ `double _pRef`) ์ถ”๊ฐ€ โ€” `cfg.PRef`๊ฐ€ NaN์ด๋ฉด ์ฒซ ์ •์ƒ ์••๋ ฅ์œผ๋กœ ์‹œ๋“œ. +- `BuildSnapshotAsync`(Supervisor)์— `cfg.TempTags` PV ์ฝ๊ธฐ๋ฅผ ์ถ”๊ฐ€(์ด๋ฏธ feed/pressure/levels/streams ์ฝ๋Š” ํŒจํ„ด ์žฌ์‚ฌ์šฉ). `PvSnapshot`์— `IReadOnlyList Temps`๋ฅผ ์ถ”๊ฐ€(positional์ด๋ฏ€๋กœ **์ƒˆ record ํ•„๋“œ๋Š” init ํ”„๋กœํผํ‹ฐ๋กœ** ์ถ”๊ฐ€ํ•˜๊ฑฐ๋‚˜ `PvSnapshot`๋ฅผ ํ™•์žฅ โ€” ๋ณธ ๋ฌธ์„œ๋Š” `PvSnapshot`์— `Temps` init ํ”„๋กœํผํ‹ฐ ์ถ”๊ฐ€ ๊ถŒ์žฅ). +- Tick์—์„œ ๊ฐ ์˜จ๋„์— PCT ๊ณ„์‚ฐ โ†’ `AdvisoryResult.Temps`(`TempPoint`)๋กœ ์ €์žฅ. `dTdP==0`์ด๋ฉด PCT=raw(์ƒ์˜จ๋„, ๋ชจ๋‹ˆํ„ฐ๋งŒ). +- **advisory-only**: Temps๋Š” ํ‘œ์‹œยทP-3 ์ž…๋ ฅ์ผ ๋ฟ ๊ถŒ์žฅSP์— ์˜ํ–ฅ ์—†์Œ(์ด๋ฒˆ WO ํ•œ์ •). + +**ConfigStore/Controller/DDL**: ยง0์˜ `temp_tags`ยท`dtdp`ยท`p_ref`ยท`sensitive_tray_tag` ๋ฐ˜์˜(load/save/map). + +**UI(ff.js)**: ์นด๋“œ ํ•˜๋‹จ์— ์˜จ๋„ ๋ฏธ๋‹ˆํ–‰(ํƒœ๊ทธยทrawยทPCT) 1์ค„. ์—†์œผ๋ฉด ์ƒ๋žต. + +**ํ…Œ์ŠคํŠธ**: `TempCorrection`(P ์ƒ์Šน ์‹œ PCT๊ฐ€ raw๋ณด๋‹ค ๋‚ฎ์•„์ง, dTdP=0์ด๋ฉด PCT=raw), `DiffTemp.Delta/Double` ์‚ฐ์ˆ , `PRef` NaN ์‹œ๋“œ ๊ฒฝ๋กœ. + +**๊ฒ€์ฆ**: dtdp>0 ์ปฌ๋Ÿผ์—์„œ ์ง„๊ณต ํ”๋“ค ๋•Œ raw๋Š” ์ถœ๋ ์ด๋‚˜ PCT๋Š” ํ‰ํƒ„(๊ณตํ†ต๋ชจ๋“œ ์ƒ์‡„) ํ™•์ธ. + +--- + +## WO-3 โ€” P-1 ฮธ ์ž๋™ํŠœ๋‹ (3์ˆœ์œ„, ๋…ธ๋ ฅ ๅคงยทํ†ต๊ณ„) + +**๋ชฉ์ **: seed ฮธ/ฯ„๊ฐ€ ์ „๋ถ€ placeholder(PhaseI ยง5.8 ๊ฒฝ๊ณ )์ธ ๋ฌธ์ œ๋ฅผ ํ•ด์†Œ. **์ •์ƒ ์šด์ „ ์ค‘ ์ž์—ฐ์™ธ๋ž€**์œผ๋กœ ฮธ๋ฅผ **passive ์‹๋ณ„**ํ•ด **์ œ์•ˆ๋งŒ**(์ž๋™๋ฐ˜์˜ ๊ธˆ์ง€ยท์šด์ „์› ์Šน์ธ ์‹œ config ๋ฐ˜์˜). +**๊ทผ๊ฑฐ**: spec ยง13.4(๊ต์ฐจ์ƒ๊ด€ ฮธ, ์ŠคํŒ€ ๋ถ€๋ถ„์ƒ๊ด€์œผ๋กœ ํ๋ฃจํ”„ ์˜ค์—ผ ํšŒํ”ผ), ยง13.7(ฮธ๋Š” ์‹ ๋ขฐ๋„ ๋“ฑ๊ธ‰ ๋ถ™์€ ์ถ”์ •์น˜). + +> **ํ˜„์‹ค ๊ฒฝ๊ณ (spec ยง13.2)**: ๋‹จ์ผ์  ์ƒ์˜จ๋„ SNR ๋‚ฎ์Œ โ†’ **WO-2์˜ PCT/ฮ”T๋ฅผ ์ž…๋ ฅ์œผ๋กœ** ์“ฐ๊ณ , **์ŠคํŒ€ OP(`SteamOpTag`)๋ฅผ 2๋ฒˆ์งธ ์ž…๋ ฅ์œผ๋กœ ๋ถ€๋ถ„์ƒ๊ด€**ํ•ด TICA ํ๋ฃจํ”„ ๋™ํŠน์„ฑ์„ ฮธ๋กœ ์˜ค๊ท€์†ํ•˜์ง€ ์•Š๊ฒŒ ํ•œ๋‹ค. ์™ธ๋ž€ ๋ถ€์กฑ ์‹œ **์‹ ๋ขฐ๋„ ๋‚ฎ์Œ โ†’ ์ œ์•ˆ ์–ต์ œ(null)**. + +**์‹ ๊ทœ ๋ธ”๋ก (`ComputationBlocks.cs` ๋˜๋Š” ์‹ ๊ทœ `CrossCorrLagEstimator.cs`)** โ€” ๊ณ„์•ฝ(์‹œ๊ทธ๋‹ˆ์ฒ˜) ๊ณ ์ •: +```csharp +/// +/// Passive ์ „๋‹ฌ์ง€์—ฐ ์‹๋ณ„. ฮ”F(ํ”ผ๋“œ ๋ณ€ํ™”)์™€ ฮ”S_i(=ฮ”PCT/ฮ”flow) ์˜ ๊ต์ฐจ์ƒ๊ด€ ์ตœ๋Œ€ ์ง€์—ฐ = ฮธ. +/// ์ŠคํŒ€ ฮ”S_steam ์„ 2๋ฒˆ์งธ ์ž…๋ ฅ์œผ๋กœ ๋ถ€๋ถ„์ƒ๊ด€(partial corr)ํ•ด ํ๋ฃจํ”„ ์˜ค์—ผ ์ œ๊ฑฐ(ยง13.4). +/// ์‚ฌ์ „๋ฐฑ์ƒ‰ํ™”(pre-whitening=1์ฐจ์ฐจ๋ถ„) ์ ์šฉ. I/O ์—†์Œ, ์ปฌ๋Ÿผ ๋ฃจํ”„ ๋‹จ์ผ ์†Œ์œ (๋ฝ ๋ถˆํ•„์š”). +/// +public sealed class CrossCorrLagEstimator +{ + public CrossCorrLagEstimator(int maxLagSamples, int historySamples, double minSignalStd); + /// ๋งค ํ‹ฑ ํ˜ธ์ถœ. ๋ฐ˜ํ™˜: ์ถฉ๋ถ„ํ•œ ์™ธ๋ž€ ๋ˆ„์  ํ›„์—๋งŒ (ฮธup,ฮธdn,conf), ์•„๋‹ˆ๋ฉด null. + public (double thetaUpSec, double thetaDnSec, double conf)? Push( + double dFeed, double dResponse, double dSteam, double tsSec); +} +``` +**์•Œ๊ณ ๋ฆฌ์ฆ˜(๊ตฌํ˜„ ์ง€์นจ)**: +1. ์ž…๋ ฅ์€ **1์ฐจ์ฐจ๋ถ„(ฮ”)** ๋ฐ›์Œ(๋ฏธ๋ถ„=์‚ฌ์ „๋ฐฑ์ƒ‰ํ™”). ๋ง๋ฒ„ํผ(historySamples)์— `dFeed`,`dResponse`,`dSteam` ๋ˆ„์ . +2. ์™ธ๋ž€ ๊ฒ€์ •: `std(dFeed) < minSignalStd` โ†’ ์‹ ๋ขฐ 0, null ๋ฐ˜ํ™˜(์–ต์ œ). +3. **๋ถ€๋ถ„์ƒ๊ด€**: `dResponse`์—์„œ `dSteam` ์„ ํ˜•ํšŒ๊ท€ ์„ฑ๋ถ„ ์ œ๊ฑฐ(์ž”์ฐจ `r = dResponse โˆ’ ฮฒยทdSteam`, ฮฒ=cov/var). ์ดํ›„ `ฯ(ฯ„)=corr(dFeed[t], r[t+ฯ„])` for ฯ„โˆˆ[0,maxLag]. +4. `ฮธ = argmax_ฯ„ ฯ(ฯ„)ยทts`. `conf = max ฯ`(0~1, ์Œ์ˆ˜๋ฉด 0). +5. ์ƒ์Šน/ํ•˜๊ฐ• ๋น„๋Œ€์นญ: `dFeed>0` ํ‘œ๋ณธ๋งŒ์œผ๋กœ ฮธup, `dFeed<0` ํ‘œ๋ณธ๋งŒ์œผ๋กœ ฮธdn ๋ณ„๋„ ์ถ”์ •(ํ‘œ๋ณธ ๋ถ€์กฑ ์‹œ ๊ณตํ†ต๊ฐ’). +6. `conf < 0.3`๋ฉด ์ œ์•ˆ ์–ต์ œ(null). + +**๋ฐฐ์„ **: `StreamState`์— ์ปฌ๋Ÿผ๋‹น 1๊ฐœ estimator(๋˜๋Š” commanded ์ŠคํŠธ๋ฆผ๋ณ„). Tick์—์„œ `cfg.ThetaAutoTune && WO-2 PCT ๊ฐ€์šฉ` ์ผ ๋•Œ๋งŒ `Push` โ†’ `StreamAdvisory.ThetaSuggestUpSec/DnSec/Conf` ์ฑ„์›€. **config ฮธ๋Š” ๋ณ€๊ฒฝํ•˜์ง€ ์•Š์Œ**(์ œ์•ˆ ์ „์šฉ). + +**UI**: ์นด๋“œ์— "ฮธ ์ œ์•ˆ โ†‘NNs โ†“NNs (conf 0.x)" ๋ณด์กฐํ–‰. ์šด์ „์›์ด ์„ค์ • ์—๋””ํ„ฐ์—์„œ ์ˆ˜๋™ ๋ฐ˜์˜. + +**ํ…Œ์ŠคํŠธ**: ํ•ฉ์„ฑ ์‹ ํ˜ธ(์•Œ๋ ค์ง„ ฮธ๋กœ ์ง€์—ฐ๋œ ์‘๋‹ต + ๋…ธ์ด์ฆˆ)์— ๋Œ€ํ•ด ์ถ”์ • ฮธ๊ฐ€ ยฑ1 ์ƒ˜ํ”Œ ๋‚ด. ์™ธ๋ž€ std ๋ฏธ๋‹ฌ ์‹œ null. ์ŠคํŒ€ ์ƒ๊ด€ ์ฃผ์ž… ์‹œ ๋ถ€๋ถ„์ƒ๊ด€์ด ์ œ๊ฑฐํ•˜๋Š”์ง€(์ŠคํŒ€๋งŒ ์ƒ๊ด€๋œ ๊ฐ€์งœ ์ง€์—ฐ์€ conf ๋‚ฎ๊ฒŒ). + +**๊ฒ€์ฆ**: ๋ผ์ด๋ธŒ์—์„œ ThetaAutoTune=true ์ปฌ๋Ÿผ์ด ์™ธ๋ž€ ์ถฉ๋ถ„ ์‹œ์—๋งŒ ์ œ์•ˆ ๋…ธ์ถœ, config ๋ฌด๋ณ€๊ฒฝ(์“ฐ๊ธฐ 0๊ฑด) ํ™•์ธ. + +> **ํ˜„์‹ค์„ฑ**: ๋ฐ๋ชจ ์‹œ์Šคํ…œ ์˜จ๋„๋Š” ์ธ์œ„ ์ƒ์„ฑ(spec ยง13.7) โ†’ ์‹คํ”Œ๋žœํŠธ ์ „ **๊ฒ€์ฆ ๋ณด๋ฅ˜ ๊ฐ€๋Šฅ**. ๋ณธ WO๋Š” **์ธํ„ฐํŽ˜์ด์Šคยท๋ธ”๋กยทํ…Œ์ŠคํŠธ๊นŒ์ง€** ํ„ดํ‚ค๋กœ ๋‘๋˜, ๊ฐ€๋™ ์Šค์œ„์น˜(`ThetaAutoTune`)๋Š” ๊ธฐ๋ณธ false. + +--- + +## WO-4 โ€” P-4 ๋А๋ฆฐ ๋ฐ”์ด์–ด์Šค ์ ์‘ (4์ˆœ์œ„) + +**๋ชฉ์ **: ๊ณ„์ ˆ CW ์Šค์œ™ ๋“ฑ **ํฌ์ง€๋งŒ ๋А๋ฆฐ ์™ธ๋ž€**(spec ยง14.4)์„ ์ •๋ฐ€๋ชจ๋ธ ๋Œ€์‹  **์žฅ๊ธฐ MA๋กœ K_obsยทk_V ์ถ”์„ธ**๋ฅผ ๋‚ด์–ด ์šด์ „์›์—๊ฒŒ "๊ณ„์ ˆ ๋ณด์ • ์ œ์•ˆ". ์ž๋™ ๋ณ€๊ฒฝ ์•„๋‹˜. +**๊ทผ๊ฑฐ**: spec ยง14.4(๋А๋ฆฐ ๋ฐ”์ด์–ด์Šค/์šด์ „์› ํŠธ๋ฆผ), ยง14.3 B๋“ฑ๊ธ‰(V_loss๋Š” ์žฅ๊ธฐ MA๋กœ๋งŒ ์˜๋ฏธ). + +**์„ค๊ณ„**: +- `ColumnState`์— `MovingAverage`(์ฐฝ=`BiasMaWindowSec/ScanSec` ์ƒ˜ํ”Œ) 2๊ฐœ: `VLossMa`, ๊ทธ๋ฆฌ๊ณ  commanded ์ŠคํŠธ๋ฆผ๋ณ„ `KObsMa`(=PV/FeedFiltered ์˜ MA). +- **์ •์ƒ์ƒํƒœ์—์„œ๋งŒ ๊ฐฑ์‹ **(transientยทBAD ์ œ์™ธ) โ€” ๊ณผ๋„ ํ‘œ๋ณธ ์˜ค์—ผ ๋ฐฉ์ง€. +- ์‚ฐ์ถœ: `AdvisoryResult.VLossMa`, `StreamAdvisory.KObsSuggest`(= K_obs MA, config TargetCoeff์™€ ๋น„๊ตํ•ด ๋“œ๋ฆฌํ”„ํŠธ ํ‘œ์‹œ). +- **advisory-only**: ์ œ์•ˆ๊ฐ’์ผ ๋ฟ ์—”์ง„ K๋Š” config ๊ทธ๋Œ€๋กœ. + +**ํ…Œ์ŠคํŠธ**: ์ผ์ • ๋น„์œจ ์ž…๋ ฅ ์Šคํ… ํ›„ MA๊ฐ€ ์ฒœ์ฒœํžˆ ์ˆ˜๋ ด(์ฐฝ ๊ธธ์ด๋งŒํผ), ๊ณผ๋„ ํ‘œ๋ณธ์€ MA์— ์•ˆ ๋“ค์–ด๊ฐ. + +**๊ฒ€์ฆ**: ์žฅ๊ธฐ ๊ฐ€๋™ ํ›„ KObsSuggest๊ฐ€ config K ๋ถ€๊ทผ, ์ธ์œ„์  bias ์ฃผ์ž… ์‹œ ์ถ”์„ธ ์ด๋™. + +> WO-4์˜ `VLossMa`๋Š” **WO-6 ์ „ํ™˜๋ฅ˜ ํŠธ๋ฆฌ๊ฑฐ ์ž…๋ ฅ**์œผ๋กœ ์žฌ์‚ฌ์šฉ(์ˆœ๊ฐ„ V_loss๋Š” ยง5.3๋Œ€๋กœ ์‹ ๋ขฐ๋ถˆ๊ฐ€ โ†’ MA๋กœ ํŒ์ •). + +--- + +## WO-5 โ€” P-3 Sweet-Spot / ํ”„๋ก ํŠธ ์œ„์น˜ ์ง€ํ‘œ (5์ˆœ์œ„, P-2 ์˜์กด) + +**๋ชฉ์ **: WO-2์˜ ์ œํ’ˆ์กด PCT/ฮ”T(๋ฏผ๊ฐํŠธ๋ ˆ์ด)๋ฅผ **ํ”„๋ก ํŠธ ์œ„์น˜ ํ”„๋ก์‹œ**๋กœ ์‚ผ์•„ ๋“œ๋ฆฌํ”„ํŠธ ์‹œ **ํ™˜๋ฅ˜/boilup ํŠธ๋ฆผ ๊ถŒ์žฅ**(advisory). analyzer ์žˆ์œผ๋ฉด ์šฐ์„ . +**๊ทผ๊ฑฐ**: spec ยง13.5(2์ธต ๊ตฌ์กฐ: ๋น ๋ฅธ ์—๋„ˆ์ง€=ํ”ผ๋“œํฌ์›Œ๋“œ, ๋А๋ฆฐ ์กฐ์„ฑ=์˜จ๋„ ํ”ผ๋“œ๋ฐฑ), ยง13.2 ํ•จ์ •โ‘ก(์ œํ’ˆ์กด ์‹ ํ˜ธ ์•ฝํ•จ โ†’ ์ฐจ์˜จ ํ•„์ˆ˜), ยง14.3 C๋“ฑ๊ธ‰. + +**์‹ ๊ทœ ๋ธ”๋ก (`FrontPositionIndicator.cs`)**: +```csharp +/// ์ œํ’ˆ์กด PCT/ฮ”T ์˜ ๊ธฐ์ค€๋Œ€๋น„ ๋“œ๋ฆฌํ”„ํŠธ โ†’ sweet-spot ๊ฑด์ „์„ฑ + ํŠธ๋ฆผ ๋ฐฉํ–ฅ ๊ถŒ์žฅ. advisory. +public sealed class FrontPositionIndicator +{ + public FrontPositionIndicator(double bandwidth, double refTau); + public (string state, string? trimAdvice, Confidence grade) Update( + double frontMetric, double tsSec); // frontMetric = ๋ฏผ๊ฐํŠธ๋ ˆ์ด PCT ๋˜๋Š” ์ œํ’ˆ์กด ฮ”T +} +``` +- ๋А๋ฆฐ ๊ธฐ์ค€(EMA refTau, ์˜ˆ 30~60min)์—์„œ `frontMetric` ์ดํƒˆ๋Ÿ‰ ์‚ฐ์ถœ. +- ๋ฐด๋“œ ๋‚ด=ใ€Œ์ •์ƒใ€ / ์œ„๋กœ ๋“œ๋ฆฌํ”„ํŠธ=ใ€Œํ”„๋ก ํŠธ ์ƒ์Šน โ€” ๊ฒฝ๋น„๋ฌผ ํ˜ผ์ž… ์œ„ํ—˜: ํ™˜๋ฅ˜โ†‘ ๊ถŒ์žฅใ€(๋ธŒ๋ ˆ์ธ์Šคํ† ๋ฐ Q2/Q3 ์ •์„) / ์•„๋ž˜=ใ€Œํ”„๋ก ํŠธ ํ•˜๊ฐ• โ€” boilupโ†‘/ํ™˜๋ฅ˜โ†“ ๊ถŒ์žฅใ€. +- ๋“ฑ๊ธ‰: ๋‹จ์ผ ์ƒ์˜จ๋„๋ฉด C(์‹ ํ˜ธ ์•ฝํ•จ), ์ฐจ์˜จ/analyzer๋ฉด B ์ด์ƒ. +- **ํŠธ๋ฆผ์€ ๊ถŒ์žฅ ๋ฌธ๊ตฌ๋งŒ**(`AdvisoryResult.FrontPositionState`/`FrontTrimAdvice`) โ€” SP ๋ฏธ๋ณ€๊ฒฝ. + +**๋ฐฐ์„ **: WO-2 Temps์—์„œ `SensitiveTrayTag`(์—†์œผ๋ฉด ์ƒ-ํ•˜ ฮ”T) ์ถ”์ถœ โ†’ Indicator.Update โ†’ AdvisoryResult ํ•„๋“œ. + +**UI**: ์นด๋“œ ๋ฐฐ๋„ˆ์— ํ”„๋ก ํŠธ ์ƒํƒœ/ํŠธ๋ฆผ ๊ถŒ์žฅ ํ‘œ์‹œ(P-6 `ff-note` ์ž๋ฆฌ ํ™œ์šฉ). + +**ํ…Œ์ŠคํŠธ**: ๊ธฐ์ค€ ๋Œ€๋น„ ์ƒ/ํ•˜ ๋“œ๋ฆฌํ”„ํŠธ์— ๋Œ€ํ•œ stateยทtrim ๋ถ„๊ธฐ, ๋ฐด๋“œ ๋‚ด ใ€Œ์ •์ƒใ€, ๋“ฑ๊ธ‰ ๊ฐ•๋“ฑ(์ƒ์˜จ๋„โ†’C). + +--- + +## WO-6 โ€” ์ „ํ™˜๋ฅ˜(Total Reflux) ํ‰ํ˜•๋ณต๊ท€ ๋ชจ๋“œ โ˜… ์‹ ๊ทœ + +**์š”๊ตฌ(์šด์ „์›)**: ์ปฌ๋Ÿผ ๊ท ํ˜•์ด **์‹ฌ๊ฐํ•˜๊ฒŒ ๊นจ์กŒ๋‹ค๊ณ  ํŒ๋‹จ๋˜๋ฉด**, **์ „ํ™˜๋ฅ˜ ๋ชจ๋“œ**๋กœ ์ „ํ™˜ โ€” **์ œํ’ˆ(P)ยท์›๋ฃŒํˆฌ์ž…(F)ยท๊ฒฝ๋น„๋ฌผ(D)ยท์ค‘๋น„๋ฌผ(B) ์ œ๊ฑฐ๋ฅผ ๋ชจ๋‘ ์ฐจ๋‹จ**ํ•˜๊ณ  **ํ™˜๋ฅ˜(R)๋ฅผ ์ „๋Ÿ‰ ํ™˜๋ฅ˜**๋กœ ๋‘์–ด **ํ‰ํ˜• ๋ณต๊ท€**ํ•  ๋•Œ๊นŒ์ง€ ์œ ์ง€, ํšŒ๋ณต ํ›„ ์ •์ƒ ๋ณต๊ท€. + +**๊ณต์ • ๊ทผ๊ฑฐ**: ์ฆ๋ฅ˜ ์ •์„์˜ *total reflux* ํšŒ๋ณต๊ธฐ๋™ = `knowledge/PGMEA_์ธก๋ฅ˜์ถ”์ถœ์šด์ „๋ฐฉ์‹_์ฃผ์˜์ .md ยง4.3`("์™ธ๋ž€ ์‹œ: ์ธก๋ฅ˜ ๋จผ์ € ์ค‘๋‹จ โ†’ ํ™˜๋ฅ˜๋น„โ†‘ ์žฌ์•ˆ์ •ํ™” โ†’ ํšŒ๋ณต ํ›„ ์žฌ๊ฐœ")์˜ ๊ทน๋‹จํ˜•. ์™ธ๋ž€์œผ๋กœ ์กฐ์„ฑ ํ”„๋ก ํŠธ๊ฐ€ ๋ฌด๋„ˆ์ง€๋ฉด, ๋“œ๋กœ์šฐยทํ”ผ๋“œ๋ฅผ ๋Š๊ณ  ๋‚ด๋ถ€ ํ™˜๋ฅ˜๋งŒ ์ˆœํ™˜์‹œํ‚ค๋ฉด ๋‹จ๋ณ„ ์กฐ์„ฑ ํ”„๋กœํŒŒ์ผ์ด **์žฌํ‰ํ˜•**๋œ๋‹ค. ์ธก๋ฅ˜์ถ”์ถœํƒ‘์€ ์ธก๋ฅ˜ ์กฐ์„ฑ์ด ์ƒ๋ถ€ ๊ฒฝ๋น„๋ฌผยทํ•˜๋ถ€ ์ค‘๋น„๋ฌผ **๊ท ํ˜•์— ์ „์  ์˜์กด**(๋™ ยง2)ํ•˜๋ฏ€๋กœ ๊ท ํ˜• ๋ถ•๊ดด ์‹œ **์ธก๋ฅ˜ ํ˜ผ์ž…(off-spec) ์ง์ „ ํšŒ๋ณต ์นด๋“œ**. + +### 6.1 ์•„ํ‚คํ…์ฒ˜ ๊ฒฐ์ • (๋ถˆ๋ณ€์‹ ์ค€์ˆ˜) +- **Phase II ๋ฒ”์œ„ = ๊ถŒ์žฅยท์ƒํƒœํ‘œ์‹œ๊นŒ์ง€**. ์ „ํ™˜๋ฅ˜๋Š” ๋ณธ์งˆ์ ์œผ๋กœ **์“ฐ๊ธฐ ๋™์ž‘**(๋“œ๋กœ์šฐ/ํ”ผ๋“œ SP=0, ํ™˜๋ฅ˜=max)์ด๋ฏ€๋กœ, **์‹ค์ œ ์‹คํ–‰์€ PhaseIII(WriteGuard) ๊ฒฝ์œ **. ๋ณธ WO๋Š” **๋ชจ๋“œ ํŒ์ • ์ƒํƒœ๊ธฐ๊ณ„ + ๊ถŒ์žฅ SP ์‚ฐ์ถœ + UI ๊ฒฝ๋ณด/ํ‘œ์‹œ**๊นŒ์ง€(์“ฐ๊ธฐ 0๊ฑด). +- **ํŠธ๋ฆฌ๊ฑฐ ๊ถŒ์œ„ 2๋ชจ๋“œ**: `RecoveryAutoArm=false`(๊ธฐ๋ณธ) โ†’ ์—”์ง„์€ "์ „ํ™˜๋ฅ˜ ๊ถŒ์žฅ(ARMED)"๋งŒ ๋„์šฐ๊ณ  **์šด์ „์› 1ํด๋ฆญ ํ™•์ธ** ํ›„ Recovering ์ง„์ž…(PhaseIII์—์„œ ์‹ค์ œ ์“ฐ๊ธฐ). `=true` โ†’ ์ž๋™ ๊ถŒ์žฅ ์ƒํƒœ๊ธฐ๊ณ„๊ฐ€ ์ง์ ‘ Recovering ์ง„์ž…(์—ฌ์ „ํžˆ ํ‘œ์‹œ/๊ถŒ์žฅ, ์“ฐ๊ธฐ๋Š” PhaseIII gating). +- **์˜ค๋ฐœ๋™ ๋น„์šฉ ํผ**(ํ”ผ๋“œ ์ฐจ๋‹จ=์ƒ์‚ฐ์ค‘๋‹จ) โ†’ ํŠธ๋ฆฌ๊ฑฐ๋Š” **์ˆœ๊ฐ„ V_loss ๊ธˆ์ง€**(ยง5.3). ๊ณผ๋„(transient) ์ค‘์—” ํŠธ๋ฆฌ๊ฑฐ ๊ธˆ์ง€. +- **๋‹ค์‹ ํ˜ธ ํŠธ๋ฆฌ๊ฑฐ(canonical ๊ทผ๊ฑฐ: `PGMEA_์ธก๋ฅ˜์ถ”์ถœ์šด์ „๋ฐฉ์‹_์ฃผ์˜์ .md ยง3`)** โ€” "๊ท ํ˜• ์‹ฌ๊ฐ ๋ถ•๊ดด"๋Š” ๋‹จ์ผ V_loss๋ณด๋‹ค ์•„๋ž˜ **์ง€์† ์กฐ๊ฑด์˜ OR**๋กœ ํŒ์ •(์–ด๋А ํ•˜๋‚˜๋ผ๋„ `ImbalanceTriggerSec` ์ง€์† ์‹œ ARM): + - **โ‘  ๋ฌผ์งˆ์ˆ˜์ง€**: `|VLossMa|/F > ImbalanceTriggerFrac` (WO-4 ์žฅ๊ธฐ MA). + - **โ‘ก ํ”„๋ก ํŠธ(๊ฐ๋„ํŠธ๋ ˆ์ด)**: WO-5 `FrontPositionState`=์‹ฌ๊ฐ ๋“œ๋ฆฌํ”„ํŠธ(์ฃผ์˜์  ยง3 1์ˆœ์œ„ = ๊ฐ๋„ํŠธ๋ ˆ์ด ์˜จ๋„. **๊ฐ€์žฅ ์‹ ๋ขฐ๋„ ๋†’์€ ์กฐ๊ธฐ์‹ ํ˜ธ**). + - **โ‘ข ํ”Œ๋Ÿฌ๋”ฉ/๋น„์‚ฐ**: `DeltaPTag` ๊ฐ€์šฉ ์‹œ `ฮ”P > DeltaPFloodLimit` (์ฃผ์˜์  ยง3 4์ˆœ์œ„ โ€” ๋น„์‚ฐ์ด ์ œํ’ˆ ์˜ค์—ผ ์ง๊ฒฐ). + - ์‹ ํ˜ธ๋ณ„ ๊ฐ€์šฉ์„ฑ์€ config๋กœ ๊ฒฐ์ •(ํƒœ๊ทธ ์—†์œผ๋ฉด ํ•ด๋‹น ์กฐ๊ฑด ๋น„ํ™œ์„ฑ). + +### 6.2 ์ƒํƒœ๊ธฐ๊ณ„ (`ColumnMode`) +``` + โ”Œโ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€ Normal โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ” + โ”‚ |VLossMa|/F > ImbalanceTriggerFrac ์ง€์†> ImbalanceTriggerSec โ”‚ + โ”‚ AND !transient โ”‚ + โ”‚ AND (RecoveryAutoArm OR ์šด์ „์› ARM ํ™•์ธ) โ”‚ + โ–ผ โ”‚ + Recovering(์ „ํ™˜๋ฅ˜) โ”‚ + ๊ถŒ์žฅ: Fโ†’FeedRecoverySp(0), P/D/Bโ†’0(๋˜๋Š” RecoverySp), Rโ†’SpMax(์ „๋Ÿ‰) โ”‚ + dwell: ํ‰ํ˜•์ง€ํ‘œ ์•ˆ์ •(|VLossMa|/F < Fracยท0.5 AND ํ”„๋ก ํŠธ(WO-5) ์ •์ƒ) โ”‚ + ์„ RecoverySettleSec ๋™์•ˆ ์—ฐ์† ๋งŒ์กฑ โ”‚ + โ–ผ โ”‚ + Returning(๋ณต๊ท€ ๋žจํ”„) โ”‚ + ReturnRampSec ๋™์•ˆ Fยทdraw๋ฅผ ์ •์ƒ ๊ถŒ์žฅ๊ฐ’์œผ๋กœ RateLimiter ๋žจํ”„ ๋ณต์› โ”‚ + ์™„๋ฃŒ โ†’ Normal โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”˜ + (์–ด๋А ์ƒํƒœ๋“  ์šด์ „์› ์ˆ˜๋™ Normal ๋ณต๊ท€ ๊ฐ€๋Šฅ / FEED BAD ์ง€์† ์‹œ Recovering ์œ ์ง€) +``` + +### 6.3 ์—”์ง„ ๊ตฌํ˜„ (`FeedforwardEngine.cs` + `ColumnState`) +- `ColumnState`์— ์ถ”๊ฐ€: `ColumnMode Mode`, `double ImbalanceTimerSec`, `double RecoverySettleTimerSec`, `double ReturnTimerSec`, `bool OperatorArmed`(์ปจํŠธ๋กค๋Ÿฌ๊ฐ€ set). +- **์ˆœ์ˆ˜์„ฑ ์œ ์ง€**: ์ƒํƒœ ์ „์ด๋Š” Tick ๋‚ด์—์„œ `st`(๊ฐ€๋ณ€ ColumnState)๋กœ ์ฒ˜๋ฆฌ โ€” ๊ธฐ์กด SettleTimer์™€ ๋™์ผ ํŒจํ„ด(I/O ์—†์Œ). +- ์ ˆ์ฐจ(Tick ๋ง๋ฏธ, advisory ์‚ฐ์ถœ ํ›„): + 1. `severe = โ‘  || โ‘ก || โ‘ข` (์œ„ ๋‹ค์‹ ํ˜ธ; ๊ฐ€์šฉ ์‹ ํ˜ธ๋งŒ ํ‰๊ฐ€). `โ‘  frac=|VLossMa|/ff > ImbalanceTriggerFrac`, `โ‘ก WO-5 FrontPositionState ์‹ฌ๊ฐ`, `โ‘ข ฮ”P>DeltaPFloodLimit`. + 2. **Normal**: `!transient && severe` โ†’ `ImbalanceTimerSec += ts` else `=0`. `ImbalanceTimerSec โ‰ฅ ImbalanceTriggerSec` + (AutoArm || OperatorArmed) โ†’ `Mode=Recovering`, ํƒ€์ด๋จธ ๋ฆฌ์…‹, OperatorArmed=false. (AutoArm=false๋ฉด ARM ์—†์ด๋Š” "์ „ํ™˜๋ฅ˜ ๊ถŒ์žฅ(ARMED ๋Œ€๊ธฐ)" ํ‘œ์‹œ๋งŒ.) + 3. **Recovering**: ์ŠคํŠธ๋ฆผ ๊ถŒ์žฅ๊ฐ’์„ **์˜ค๋ฒ„๋ผ์ด๋“œ** โ€” reflux(IsReflux||RefluxFromProduct)=`SpMax`, ๊ทธ ์™ธ commanded draw=`RecoverySp(NaNโ†’0)`, FEED ๊ถŒ์žฅ=`FeedRecoverySp`. ํ‰ํ˜•์กฐ๊ฑด(`frac < Frac*0.5 && ํ”„๋ก ํŠธ ์ •์ƒ`) ์—ฐ์† ๋งŒ์กฑ ์‹œ `RecoverySettleTimerSec += ts`, ๋„๋‹ฌ ์‹œ `Mode=Returning`. + 4. **Returning**: `ReturnTimerSec += ts`; ์ง„ํ–‰๋ฅ  `ฮฑ=min(1, ReturnTimerSec/ReturnRampSec)`๋กœ draw/feed ๊ถŒ์žฅ์„ 0โ†’์ •์ƒ๊ฐ’ ๋ณด๊ฐ„(๋˜๋Š” RateLimiter๊ฐ€ ์ž์—ฐ ๋žจํ”„). `ฮฑ>=1` โ†’ `Mode=Normal`. + 5. `AdvisoryResult`์— `with { Mode = st.Mode, ModeReason = ..., VLossMa = ... }`. Recovering/Returning์—์„  ๊ฐ `StreamAdvisory.RecommendedSp`๊ฐ€ ์˜ค๋ฒ„๋ผ์ด๋“œ๊ฐ’, `Note`์— "์ „ํ™˜๋ฅ˜ ๋ณต๊ท€" ํ‘œ๊ธฐ. **Grade๋Š” ๊ฐ•๋“ฑํ•˜์ง€ ์•Š๋˜ Valid=false(์šด์ „์› ์ธ๊ฐ€ ํ•„์š”)**. +- **FEED ๊ถŒ์žฅ ๋…ธ์ถœ**: FEED๋Š” ์ŠคํŠธ๋ฆผ์ด ์•„๋‹ˆ๋ฏ€๋กœ `AdvisoryResult`์— `FeedRecommendedSp`(double?) init ํ”„๋กœํผํ‹ฐ ์ถ”๊ฐ€ โ€” Recovering ์‹œ `FeedRecoverySp`, ๊ทธ ์™ธ null. + +### 6.4 ์ปจํŠธ๋กค๋Ÿฌ (`FeedforwardController.cs`) +- `POST api/ff/recovery/{columnId}/arm` โ€” ์šด์ „์› ARM(=Supervisor ํ†ตํ•ด ํ•ด๋‹น ColumnState.OperatorArmed=true). **Supervisor๋ฅผ singleton ์ฃผ์ž…**(A2 ์ •์ •์œผ๋กœ ๊ฐ€๋Šฅ)ํ•ด ์ปฌ๋Ÿผ ์ƒํƒœ ์ ‘๊ทผ. ์“ฐ๊ธฐ ์•„๋‹˜(๋ชจ๋“œ ํŒ์ •์šฉ ํ”Œ๋ž˜๊ทธ). +- `POST api/ff/recovery/{columnId}/cancel` โ€” ์ˆ˜๋™ Normal ๋ณต๊ท€. +- (PhaseIII์—์„œ) ์‹ค์ œ SP ์“ฐ๊ธฐ๋Š” ๋ณ„๋„ `apply` ์—”๋“œํฌ์ธํŠธ๊ฐ€ WriteGuard ๊ฒฝ์œ . + +### 6.5 UI (ff.js / ff.css) +- ์นด๋“œ ํ—ค๋”์— ๋ชจ๋“œ ๋ฑƒ์ง€: `Normal`(๋ฌดํ‘œ์‹œ) / `์ „ํ™˜๋ฅ˜ ๋ณต๊ท€์ค‘ โ—`(์ฃผํ™ฉ) / `๋ณต๊ท€ ๋žจํ”„ โ—`(ํŒŒ๋ž‘) / `์ „ํ™˜๋ฅ˜ ๊ถŒ์žฅ(ARMED)`(์ ๋ฉธ ๊ฒฝ๋ณด + [ํ™•์ธ] ๋ฒ„ํŠผ โ†’ arm ํ˜ธ์ถœ). +- Recovering/Returning ์‹œ ํ‘œ์— "๊ถŒ์žฅ SP" ์—ด์ด 0/max ์˜ค๋ฒ„๋ผ์ด๋“œ๋กœ ํ‘œ์‹œ, Valid=false๋ผ ํ๋ฆฌ๊ฒŒ(`ff-stale`)ยท"์šด์ „์› ์ธ๊ฐ€" ์ฃผ์„. + +### 6.6 DDL/ConfigStore +ยง0์˜ recovery ์ปฌ๋Ÿผ๋“ค(`recovery_enabled`, `recovery_auto_arm`, `imbalance_trigger_frac`, `imbalance_trigger_sec`, `recovery_settle_sec`, `return_ramp_sec`, `feed_recovery_sp`) + stream `is_reflux`/`recovery_sp` ๋ฐ˜์˜. + +### 6.7 ํ…Œ์ŠคํŠธ (xUnit) +- ํ•ฉ์„ฑ ์‹œํ€€์Šค: ์ •์ƒ โ†’ VLossMa ์ž„๊ณ„ ์ง€์† ์ดˆ๊ณผ(+!transient) โ†’ `Recovering` ์ง„์ž…. AutoArm=false๋ฉด OperatorArmed ์—†์ด๋Š” **์ง„์ž… ์•ˆ ํ•จ**. +- Recovering ๊ถŒ์žฅ๊ฐ’: reflux=SpMax, P/D/B=0, FeedRecommendedSp=FeedRecoverySp. +- ํ‰ํ˜• ํšŒ๋ณต(fracโ†“ ์ง€์† RecoverySettleSec) โ†’ `Returning` โ†’ ReturnRampSec ๊ฒฝ๊ณผ โ†’ `Normal`. +- transient ์ค‘์—” ํŠธ๋ฆฌ๊ฑฐ ํƒ€์ด๋จธ ๋ˆ„์  ์•ˆ ๋จ. +- ์ˆ˜๋™ cancel ์‹œ ์ฆ‰์‹œ Normal. +- **์“ฐ๊ธฐ 0๊ฑด**(์—”์ง„/์ปจํŠธ๋กค๋Ÿฌ grep) โ€” ๋ชจ๋“œ ํŒ์ •ยท๊ถŒ์žฅ๋งŒ. + +### 6.8 ์•ˆ์ „ ๊ฒฐ์ • (๋ฌธ์„œํ™”) +| ํ•ญ๋ชฉ | ๊ฒฐ์ • | +|:-----|:-----| +| ์‹คํ–‰ ๊ถŒํ•œ | ๊ถŒ์žฅยทํ‘œ์‹œ๋Š” PhaseII, **์‹ค์ œ SP ์“ฐ๊ธฐ๋Š” PhaseIII WriteGuard** ๊ฒฝ์œ (์ „ํ™˜๋ฅ˜๋Š” ๋Œ€๊ทœ๋ชจ ์กฐ์ž‘์ด๋ผ ์šด์ „์› ํ™•์ธ ๊ฐ•์ œ ๊ถŒ์žฅ) | +| ์˜ค๋ฐœ๋™ ๋ฐฉ์ง€ | ์ˆœ๊ฐ„ V_loss ๊ธˆ์ง€, `VLossMa`(์žฅ๊ธฐ MA) ์ง€์† ์ดˆ๊ณผ + `!transient` + (AutoArm||์šด์ „์› ARM) | +| ๋ณต๊ท€ ๋ถ€๋“œ๏ฟฝ๋Ÿฌ์›€ | Returning์—์„œ RateLimiter ๋žจํ”„ โ€” bumpless | +| ํŠธ๋ฆฌ๊ฑฐ ๋ณด์ˆ˜์„ฑ | ๊ธฐ๋ณธ `RecoveryAutoArm=false`(์šด์ „์› 1ํด๋ฆญ). ์ž๋™๋ฌด์žฅ์€ ์‹ ๋ขฐ ํ™•๋ณด ํ›„ | +| point of no return | ํ”„๋ก ํŠธ(WO-5) "๊ฒฝ๋น„๋ฌผ ํ˜ผ์ž… ์œ„ํ—˜" ๋‹จ๊ณ„์—์„œ **์„ ์ œ ARM ๊ถŒ์žฅ**(์ •์„) | + +--- + +## ยงC. ํ†ตํ•ฉ ๊ฒ€์ฆ (๊ฐ๋…์ž โ€” diagnosis-checklist.md 8๋‹จ๊ณ„) + +1. **๋นŒ๋“œ**: `dotnet build src/Web/ExperionCrawler.csproj` ๊ฒฝ๊ณ 0/์—๋Ÿฌ0. +2. **์“ฐ๊ธฐ ๋ถˆ๋ณ€์‹**(FF ๊ฒฝ๋กœ ํ•œ์ •): `grep -rE "ExperionOpcWriteClient|Write.*Async" src/Infrastructure/Control src/Web/Controllers/FeedforwardController.cs` โ†’ **0๊ฑด**(๋ฒ”์šฉ `ExperionOpcWriteClient.cs`๋Š” OpcUa ํด๋”๋ผ ๋ฏธํฌํ•จ โ€” ์ •์ƒ). WO ์ „์ฒด advisory. +3. **DI(A2)**: WO-6 ์ ์šฉ ์‹œ `FeedforwardSupervisor`๊ฐ€ singleton+hosted๋กœ ๋…ธ์ถœ๋˜์–ด **์ธ์Šคํ„ด์Šค 1๊ฐœ๋งŒ ๊ฐ€๋™**(๋กœ๊ทธ ํ‹ฑ ๋ฃจํ”„ 1ํšŒ) + ์ปจํŠธ๋กค๋Ÿฌ ์ฃผ์ž… ๊ฐ€๋Šฅ. WO-6 ์ „์ด๋ฉด ํ˜„ ๋‹จ์ผ AddHostedService ์œ ์ง€. +4. **์ธ๋ฑ์Šค ์ •ํ•ฉ(ยง0)**: ConfigStore SELECT ์ปฌ๋Ÿผ โ†” `rd.GetXxx(n)` 1:1, ์ €์žฅโ†’์žฌ๋กœ๋“œ ๋ผ์šด๋“œํŠธ๋ฆฝ ์ผ์น˜(์‹ ๊ทœ ํ•„๋“œ ํฌํ•จ). +5. **๋‹จ์œ„ํ…Œ์ŠคํŠธ**: WO-1~WO-6 ์ผ€์ด์Šค + PhaseI ๊ธฐ์กด 4 ๋ชจ๋‘ green. +6. **๋ผ์ด๋ธŒ**: Tab 18์—์„œ ๋“ฑ๊ธ‰ ๊ฐ•๋“ฑ(WO-1)ยทPCT(WO-2)ยทฮธ์ œ์•ˆ(WO-3)ยทKObs์ถ”์„ธ(WO-4)ยทํ”„๋ก ํŠธ ํŠธ๋ฆผ(WO-5)ยท์ „ํ™˜๋ฅ˜ ๋ชจ๋“œ ๋ฑƒ์ง€/ARM(WO-6) ํ‘œ์‹œ. ํด๋ง ๋ˆ„์ˆ˜ ์—†์Œ. +7. **์ „ํ™˜๋ฅ˜ ์‹œ๋‚˜๋ฆฌ์˜ค**: ์ธ์œ„์  V_loss bias ์ฃผ์ž… โ†’ VLossMa ์ž„๊ณ„ ์ง€์† โ†’ (ARM ํ›„) Recovering ๊ถŒ์žฅ(R=max, F/P/D/B=0) โ†’ ํšŒ๋ณต โ†’ Returning โ†’ Normal. ์“ฐ๊ธฐ 0๊ฑด. + +--- + +## ยงD. P-7 (PhaseIII auto-write) โ€” ๊ธฐ์กด ๋ฌธ์„œ ์ •์ • ๋ฉ”๋ชจ + +PhaseIII ๋ฌธ์„œ๋Š” ๋Œ€์ฒด๋กœ ์œ ํšจํ•˜๋‚˜ ์ฐฉ์ˆ˜ ์ „ ์•„๋ž˜ ์ •์ •: +- **D1**: PhaseIII ยง6์ด ์ง€๋ชฉํ•œ `OpcUaClientService.cs`๋Š” **๊ทธ ์ด๋ฆ„์œผ๋ก  ๋ถ€์žฌ**. ๊ทธ๋Ÿฌ๋‚˜ **`src/Infrastructure/OpcUa/ExperionOpcWriteClient.cs`(namespace `ExperionCrawler.Infrastructure.Control`, `Opc.Ua.Client` ์‚ฌ์šฉ)๊ฐ€ ์ด๋ฏธ ์กด์žฌ** โ€” ์“ฐ๊ธฐ ๋ž˜ํผ๋ฅผ **์‹ ๊ทœ ์ž‘์„ฑํ•˜์ง€ ๋ง๊ณ  ์ด ๊ธฐ์กด ํด๋ผ์ด์–ธํŠธ๋ฅผ ์žฌ์‚ฌ์šฉ/ํ™•์žฅ**(๊ฐ€๋“œ๋Š” WriteGuard๋กœ ์ƒ์œ„์—์„œ). ๊ธฐ์กด ์ฝ๊ธฐ ์„œ๋น„์Šค(Realtime/History/Metadata ๋“ฑ)๋Š” ๊ฑด๋“œ๋ฆฌ์ง€ ๋ง ๊ฒƒ. +- **D2**: NodeId `ns=3;s="{tag}.sp"` ๋Š” **๋ฏธ๊ฒ€์ฆ ๊ฐ€์ •** โ€” `ExperionOpcWriteClient`์˜ ์‹ค์ œ ๋…ธ๋“œ ์ง€์ • ๋ฐฉ์‹ + ์„œ๋ฒ„ ๋ธŒ๋ผ์šฐ์ฆˆ๋กœ **ํ™•์ธ ํ›„ ํ™•์ •**. +- **D3**: WriteGuard ๊ฒŒ์ดํŠธ๋Š” **WO-1์˜ ๋™์  Grade**(์ •์  config Grade ์•„๋‹˜) + `!Transient` + SafetyMaxDelta. WO-6 ์ „ํ™˜๋ฅ˜๋Š” ๋ณ„๋„ `apply` ๊ฒฝ๋กœ๋กœ **์šด์ „์› ํ™•์ธ ๊ฐ•์ œ**. +- **D4**: PhaseIII ยง1.2("B/C ๊ธˆ์ง€")์™€ ยง4.3("์ŠคํŠธ๋ฆผ ๋‹จ์œ„ A๋งŒ ๋Œ€์ƒ")์€ **per-stream ๋™์  Grade ๊ธฐ์ค€**์œผ๋กœ ํ†ต์ผ. +- **D5**: auth ์žฌ๋„์ž…(`IKbAuthService`)์€ **์“ฐ๊ธฐ ์—”๋“œํฌ์ธํŠธ์—๋งŒ**(advisory/config ์ฝ๊ธฐ๋Š” ๊ณต๊ฐœ ์œ ์ง€). + +--- + +## ยงE. ํ„ดํ‚ค ์š”์•ฝ + +| WO | ํ•ญ๋ชฉ | ๋…ธ๋ ฅ | ์‹ ๊ทœ ํŒŒ์ผ | ๋ณ€๊ฒฝ ํŒŒ์ผ | ๊ฐ€๋™ ์Šค์œ„์น˜(๊ธฐ๋ณธ) | +|:--:|:-----|:----:|:----------|:----------|:------------------| +| A | ๋ฌธ์„œ ๋“œ๋ฆฌํ”„ํŠธ ์ •์ •(ยงB ๊ธฐ์ค€์„ ํ™”) | ๅฐ | โ€” | (WO-6 ์‹œ Program.cs DI ๊ต์ฒด) | โ€” | +| 0 | ๋ชจ๋ธยทDDL ๊ณตํ†ตํ™•์žฅ | ไธญ | โ€” | Models/ConfigStore/Context/Controller | โ€” | +| 1 | P-5 ๋“ฑ๊ธ‰ ์ž๋™๊ฐ•๋“ฑ | ๅฐ | โ€” | Engine, ff.js | ํ•ญ์ƒ | +| 2 | P-2 PCT/์ฐจ์˜จ | ๅฐ | (DiffTemp ๋ธ”๋ก) | Blocks/Engine/Supervisor/Store/Ctrl | dtdp>0 ์‹œ | +| 3 | P-1 ฮธ ์ž๋™ํŠœ๋‹ | ๅคง | CrossCorrLagEstimator | Engine/State/Supervisor | ThetaAutoTune=false | +| 4 | P-4 ๋А๋ฆฐ ๋ฐ”์ด์–ด์Šค | ไธญ | โ€” | Engine/State | ํ•ญ์ƒ(์ œ์•ˆ) | +| 5 | P-3 ํ”„๋ก ํŠธ ์œ„์น˜ | ไธญ | FrontPositionIndicator | Engine/State | ์˜จ๋„ ๊ฐ€์šฉ ์‹œ | +| 6 | ์ „ํ™˜๋ฅ˜ ๋ณต๊ท€ | ๅคง | โ€” | Engine/State/Ctrl/ff.js/css | RecoveryEnabled=false, AutoArm=false | +| D | P-7 ์ •์ • | โ€” | (PhaseIII) | โ€” | โ€” | + +**๊ตฌํ˜„ ์ˆœ์„œ**: A(์ •์ •) โ†’ 0(๊ณตํ†ตํ™•์žฅยทDDLยทConfigStore ์ธ๋ฑ์Šค๊ฒ€์ฆ) โ†’ 1 โ†’ 2 โ†’ 3 โ†’ 4 โ†’ 5 โ†’ 6 โ†’ C(๊ฒ€์ฆ). ๊ฐ WO๋Š” **๋นŒ๋“œ+ํ•ด๋‹น ํ…Œ์ŠคํŠธ green + ์“ฐ๊ธฐ 0๊ฑด grep** ํ›„ ๋‹ค์Œ ์ง„ํ–‰. + +**๋ถˆ๋ณ€์‹ ์žฌํ™•์ธ**: ๋ณธ ๋ฌธ์„œ ์ „์ฒด ๋ฒ”์œ„์—์„œ **์ œ์–ด ๋ ˆ์ง€์Šคํ„ฐ ์“ฐ๊ธฐ 0๊ฑด**. ์ „ํ™˜๋ฅ˜ ํฌํ•จ ๋ชจ๋“  ์‹ค์ œ SP ์“ฐ๊ธฐ๋Š” PhaseIII. + + + + +File created successfully: /home/windpacer/projects/ExperionCrawler/docs/์ธก๋ฅ˜์ถ”์ถœ์‹-ํ†ตํ•ฉ์œ ๋Ÿ‰์„ค์ •๊ณต์‹-๊ตฌํ˜„์ฝ”๋”ฉ-PhaseII-๋ถ„์„์—”์ง„+์ „ํ™˜๋ฅ˜๋ณต๊ท€.md \ No newline at end of file diff --git a/docs/์ธก๋ฅ˜์ถ”์ถœ์‹-ํ†ตํ•ฉ์œ ๋Ÿ‰์„ค์ •๊ณต์‹-๊ตฌํ˜„์ฝ”๋”ฉ-PhaseII.md b/docs/์ธก๋ฅ˜์ถ”์ถœ์‹-ํ†ตํ•ฉ์œ ๋Ÿ‰์„ค์ •๊ณต์‹-๊ตฌํ˜„์ฝ”๋”ฉ-PhaseII.md new file mode 100644 index 0000000..53e6aee --- /dev/null +++ b/docs/์ธก๋ฅ˜์ถ”์ถœ์‹-ํ†ตํ•ฉ์œ ๋Ÿ‰์„ค์ •๊ณต์‹-๊ตฌํ˜„์ฝ”๋”ฉ-PhaseII.md @@ -0,0 +1,514 @@ +# ์ธก๋ฅ˜์ถ”์ถœ ํ†ตํ•ฉ์œ ๋Ÿ‰ โ€” Phase II UI ๊ตฌํ˜„ ์ฝ”๋”ฉ (Tab 18: ์„ค์ • + ๊ถŒ์žฅ SP ๋Œ€์‹œ๋ณด๋“œ) + +> **์„ฑ๊ฒฉ**: Phase I advisory ์—”์ง„(`...-PhaseI.md`)์˜ **Web UI ์ฝ”๋”ฉ ๋ช…์„ธ + ๊ฒ€์ฆ ์ ˆ์ฐจ**. +> ๊ฐ๋…์ž๊ฐ€ `diagnosis-checklist.md` 8๋‹จ๊ณ„๋กœ ์ง„๋‹จํ•œ ๋’ค ๋ฐ˜์˜. **advisory ๋ถˆ๋ณ€์‹ ์œ ์ง€** โ€” ์ œ์–ด ๋ ˆ์ง€์Šคํ„ฐ ์“ฐ๊ธฐ 0๊ฑด. +> Phase II๋Š” **์šด์ „์›์ด ๊ฒฝํ—˜์ƒ์ˆ˜๋ฅผ ๊ณต๊ธ‰**ํ•˜๊ณ  **๊ถŒ์žฅ SP๋ฅผ ํ™”๋ฉด์—์„œ ๋ณธ๋‹ค**(์ˆ˜๋™ ์ธ๊ฐ€). ์ž๋™ ์“ฐ๊ธฐ๋Š” Phase III. + +**Phase II ๋ฒ”์œ„ ๋ถ„๋ฆฌ**: +- **๋ณธ ๋ฌธ์„œ = UI ์ฝ”๋”ฉ**: โ‘  ์„ค์ • CRUD API(admin) โ‘ก Tab 18 = ์„ค์ • ์—๋””ํ„ฐ + ๊ถŒ์žฅ SP ๋Œ€์‹œ๋ณด๋“œ. +- **Phase II-๋ถ„์„(๋ณ„๋„)**: ฮธ ์ž๋™ํŠœ๋‹ยทPCT/์ฐจ์˜จยทfront-positionยทconfidence ์ž๋™๊ฐ•๋“ฑยท๋А๋ฆฐ ๋ฐ”์ด์–ด์Šค(= PhaseI ยง6 P-1~P-5). ๋ณธ ๋ฌธ์„œ ยง6์— ์ธํ„ฐํŽ˜์ด์Šค ํ›…๋งŒ. + +--- + +## 0. ๊ธฐ์กด UI ์•„ํ‚คํ…์ฒ˜ ์ „์ œ (ํ™•์ธ๋จ) + +| ์š”์†Œ | ์‚ฌ์‹ค | +|:-----|:-----| +| ํƒญ ๋ผ์šฐํ„ฐ | `core.js`์˜ `paneInit` ๋งต + `activateTab(tab)` โ†’ `data-src`(`/panes/.html`) HTML์„ fetchํ•ด ์ฃผ์ž… ํ›„ `paneInit[tab]?.()` ํ˜ธ์ถœ | +| ํƒญ ๋“ฑ๋ก | โ‘  `index.html` `