diff --git a/README.md b/README.md index 7cf53f1..b22bd77 100644 --- a/README.md +++ b/README.md @@ -110,6 +110,8 @@ else if (!result.IsCancelled) - `RawValues` 和 `Scores` 始终保持公开形状:六个雷达轴加 `fitted_constant`;不可用项为 `null`。 - 只有七维 raw 全部成功时才生成 `FittedConstant` 和映射后的 score。 +- 拟合输出在模块内部限位;上下限由 `RegressionBetaModel` 的 + `MinimumFittedConstant` / `MaximumFittedConstant` 常量配置,当前为 0~18。 - `partial` 会保留已完成的特征,但不会生成拟合定数。 @@ -266,6 +268,8 @@ else if (!result.IsCancelled) axes plus `fitted_constant`; unavailable entries are `null`. - `FittedConstant` and mapped scores are produced only when all seven raw features succeed. +- Predictions are clamped inside the module using `MinimumFittedConstant` and + `MaximumFittedConstant` in `RegressionBetaModel` (currently 0 and 18). - `partial` preserves completed feature results but does not produce a fitted constant. - `fitted_constant` is identity-mapped and is not on the radar axes' 0-250 diff --git a/Runtime/Regression/RegressionBetaModel.cs b/Runtime/Regression/RegressionBetaModel.cs index e64b2f7..e57132f 100644 --- a/Runtime/Regression/RegressionBetaModel.cs +++ b/Runtime/Regression/RegressionBetaModel.cs @@ -6,6 +6,10 @@ namespace MajRadar.Regression; public sealed class RegressionBetaModel { + // Output limits can be tuned independently of the fitted coefficients. + internal const double MinimumFittedConstant = 0; + internal const double MaximumFittedConstant = 18; + public IReadOnlyList InputFeatures => RadarFeatureNames.ModelInputOrder; public double Predict(IReadOnlyList rawValues) @@ -32,7 +36,7 @@ public double Predict(IReadOnlyList rawValues) result += coefficients[coefficientIndex++] * normalized[left] * normalized[right]; if (coefficientIndex != coefficients.Length || double.IsNaN(result) || double.IsInfinity(result)) throw new InvalidOperationException("Regression prediction is invalid."); - return result; + return Math.Clamp(result, MinimumFittedConstant, MaximumFittedConstant); } public double Predict(IReadOnlyDictionary rawFeatures) diff --git a/Tests~/MajRadar.Tests/AnalysisAndRegressionTests.cs b/Tests~/MajRadar.Tests/AnalysisAndRegressionTests.cs index e183d12..d94a2ec 100644 --- a/Tests~/MajRadar.Tests/AnalysisAndRegressionTests.cs +++ b/Tests~/MajRadar.Tests/AnalysisAndRegressionTests.cs @@ -360,14 +360,19 @@ public void FeatureFailuresAreIsolatedInsideRadarAnalyzer() [Theory] [MemberData(nameof(RegressionVectors))] - public void EmbeddedRegressionMatchesFrozenPythonVectors(double[] raw, double expected) + public void EmbeddedRegressionMatchesPythonVectorsWithinOutputLimits(double[] raw, double expected) { var actual = new RegressionBetaModel().Predict(raw); - Assert.Equal(expected, actual, 9); + Assert.Equal(Math.Clamp(expected, + RegressionBetaModel.MinimumFittedConstant, + RegressionBetaModel.MaximumFittedConstant), actual, 9); } public static IEnumerable RegressionVectors() { + // Unbounded Python oracle outputs exercise both ends of the configured range. + yield return Vector(new[] { 8.0, 15.0, 20000.0, 10.0, 3.0, 10.0, 1.0 }, -136224.7610705242); + yield return Vector(new[] { 8.0, 15.0, 20.0, 10.0, 3.0, 10.0, 1000.0 }, 435992.9429645393); yield return Vector(new[] { 6.727708533441772, 10.213333333333333, 0.0, 3.500000000000045, 2.3435703486210016, 5.223136339994041, 0.32687803060913356 }, 13.18826234659077); yield return Vector(new[] { 5.3448652850017435, 9.373333333333333, 5.151860505357921, 3.8688372913206157, 1.527537949608422, 3.4463156768134153, 0.5098107142089721 }, 13.04867082557079); yield return Vector(new[] { 6.556341447489688, 9.728309501643864, 2.7754445459480404, 4.146342997793008, 1.977721867042476, 10.365354504785971, 0.276398788581657 }, 13.335359785629928);