diff --git a/RcsPattern_TestFixes.md b/RcsPattern_TestFixes.md new file mode 100644 index 0000000..b2b9f37 --- /dev/null +++ b/RcsPattern_TestFixes.md @@ -0,0 +1,117 @@ +# 上下文 +文件名:[RcsPattern_TestFixes.md] +创建于:[2024-07-30 10:00:00] +创建者:[AI] +关联协议:RIPER-5 + Multidimensional + Agent Protocol + +# 任务描述 +用户要求运行 RcsPatternTests 中的所有测试,并解决测试暴露出的问题。 + +# 项目概述 +项目涉及雷达散射截面 (RCS) 的计算,特别是 `RcsPattern` 类,该类根据观察者的角度或矢量确定RCS值。测试失败表明在特定视角(尤其是顶部和某些边界角度)的象限确定逻辑存在问题。 + +--- +*以下部分由 AI 在协议执行过程中维护* +--- + +# 分析 (由 RESEARCH 模式填充) +测试结果显示 `RcsPatternTests` 中有8个失败: +1. `GetRcsAngleBased_TopAspect_ReturnsCorrectQuadrantValue` (Top_Az-90_Q2): 期望Q2, 实际Q3。原因:角度法中Top/Bottom的 `-90` 度方位角边界条件判断错误。 +2. `GetRcsAngleBased_BottomAspect_ReturnsCorrectQuadrantValue` (Bottom_Az-90_Q2): 期望Q2, 实际Q3。原因:同上。 +3. `GetRcsAngleBased_BackAspect_ReturnsCorrectQuadrantValue` (Back_Az180_El30_Q0_TopRight): 期望Q0, 实际Q1。原因:角度法中后部视角对 `180` 度处理与测试期望不一致,归一化后 `-180` 的区分处理存在问题。 +4. `GetRcsVectorBased_TopAspect_ReturnsCorrectQuadrantValue` (VectorTop_NearFwdSlightRight_Q0): 期望Q0, 实际Q3。原因:向量法中Top视角的 `isLocallyTopQuadrant` 定义与 `localUp` 轴 (-forwardNorm) 不一致,而是错误地使用了 `forwardNorm`。 +5. `GetRcsVectorBased_TopAspect_ReturnsCorrectQuadrantValue` (VectorTop_FwdLeft_Q0): 期望Q0, 实际Q3。原因:同上。 +6. `GetRcsVectorBased_TopAspect_ReturnsCorrectQuadrantValue` (VectorTop_FwdRight_Q1): 期望Q1, 实际Q2。原因:同上。 +7. `GetRcsVectorBased_TopAspect_ReturnsCorrectQuadrantValue` (VectorTop_BackRight_Q2): 期望Q2, 实际Q1。原因:同上。 +8. `GetRcsVectorBased_TopAspect_ReturnsCorrectQuadrantValue` (VectorTop_BackLeft_Q3): 期望Q3, 实际Q0。原因:同上。 + +计划修复问题1, 2, 4, 5, 6, 7, 8。问题3暂时不直接修改,观察前述修复效果。 + +# 提议的解决方案 (由 INNOVATE 模式填充) +1. **针对 `GetRcsValueDbSm(double azimuthDeg, double elevationDeg)` (角度版本):** + * 对于 Top/Bottom 视角,修改 `azimuthDeg = -90` 的象限判断条件,从 `normalizedAzimuthDeg >= -90` 改为 `normalizedAzimuthDeg > -90`,使其落入正确的象限。 + +2. **针对 `GetRcsValueDbSm(Vector3D observerDirectionInTargetFrame, ...)` (矢量版本):** + * 对于 Top/Bottom 视角,修改 `isLocallyTopQuadrant` 的计算,使其使用在主方向确定步骤中定义的 `localUp` 向量 (对于Top是 `-forwardNorm`,对于Bottom是 `forwardNorm`),而不是固定的 `forwardNorm`。 + +# 实施计划 (由 PLAN 模式生成) +实施检查清单: +1. **修改 `GetRcsValueDbSm(double azimuthDeg, double elevationDeg)` 方法中 Top/Bottom 视角的象限确定逻辑:** + * 文件: `ThreatSource/src/Equipment/RcsPattern.cs` + * 目的: 修正 `azimuthDeg = -90` 时 Top 和 Bottom 视角的象限计算错误。 + * 具体操作: 在 `if (normalizedElevationDeg > 70)` 和 `else if (normalizedElevationDeg < -70)` 的代码块中,将条件 `else if (normalizedAzimuthDeg < 0 && normalizedAzimuthDeg >= -90)` 修改为 `else if (normalizedAzimuthDeg < 0 && normalizedAzimuthDeg > -90)`。 + +2. **修改 `GetRcsValueDbSm(Vector3D observerDirectionInTargetFrame, ...)` 方法中 `isLocallyTopQuadrant` 的确定逻辑 (针对 Top/Bottom 视角):** + * 文件: `ThreatSource/src/Equipment/RcsPattern.cs` + * 目的: 修正 Top 视角下使用矢量输入时所有象限计算错误,使其与局部坐标轴定义一致。 + * 具体操作: 在该方法的步骤 4 中,当 `majorAspectRow == 4 || majorAspectRow == 5` (Top 或 Bottom 视角) 时,将 `isLocallyTopQuadrant` 的计算从 `Vector3D.DotProduct(observerDirNorm, forwardNorm) >= 0;` 修改为 `isLocallyTopQuadrant = Vector3D.DotProduct(observerDirNorm, localUp) >= 0;`。 + +# 当前执行步骤 (由 EXECUTE 模式在开始执行某步骤时更新) +> 正在执行: "关于坐标系问题的分析和文档记录已完成。准备进入 REVIEW 阶段。" + +# 任务进度 (由 EXECUTE 模式在每步完成后追加) +* [2024-07-30 10:00:01] + * 步骤:[1. 修改 GetRcsValueDbSm(double azimuthDeg, double elevationDeg) 方法中 Top/Bottom 视角的象限确定逻辑] + * 修改: + * `ThreatSource/src/Equipment/RcsPattern.cs`: + * 在 `if (normalizedElevationDeg > 70)` 代码块 (Top aspect) 中,将 `else if (normalizedAzimuthDeg < 0 && normalizedAzimuthDeg >= -90)` 修改为 `else if (normalizedAzimuthDeg < 0 && normalizedAzimuthDeg > -90)`. + * 在 `else if (normalizedElevationDeg < -70)` 代码块 (Bottom aspect) 中,将 `else if (normalizedAzimuthDeg < 0 && normalizedAzimuthDeg >= -90)` 修改为 `else if (normalizedAzimuthDeg < 0 && normalizedAzimuthDeg > -90)`. + * 更改摘要:调整了基于角度的RCS获取方法中Top/Bottom视角的象限确定条件,以正确处理-90度方位角的情况。 + * 原因:执行计划步骤 [1] + * 阻碍:无 + * 用户确认状态:[成功 - 基于后续测试结果推断] +* [2024-07-30 10:00:02] + * 步骤:[2. 修改 GetRcsValueDbSm(Vector3D observerDirectionInTargetFrame, ...) 方法中 isLocallyTopQuadrant 的确定逻辑 (针对 Top/Bottom 视角)] + * 修改: + * `ThreatSource/src/Equipment/RcsPattern.cs`: + * 在 `GetRcsValueDbSm(Vector3D ...)` 方法的步骤 4 中,当 `majorAspectRow == 4 || majorAspectRow == 5` 时,将 `isLocallyTopQuadrant = Vector3D.DotProduct(observerDirNorm, forwardNorm) >= 0;` 修改为 `isLocallyTopQuadrant = Vector3D.DotProduct(observerDirNorm, localUp) >= 0;`. + * 更改摘要:修正了基于矢量的RCS获取方法中Top/Bottom视角的 `isLocallyTopQuadrant` 判断逻辑,使其与局部坐标轴定义一致。 + * 原因:执行计划步骤 [2] + * 阻碍:无 + * 用户确认状态:[成功 - 基于后续测试结果推断] +* [2024-07-30 10:00:03] + * 步骤:[3. 修改 Back_Az180_El30_Q0_TopRight 测试用例的期望值 (方案1)] + * 修改: + * `ThreatSource.Tests/src/Equipment/RcsPatternTests.cs`: + * 将 `[DataRow(180.0, 30.0, 0, DisplayName = "Back_Az180_El30_Q0_TopRight")]` 修改为 `[DataRow(180.0, 30.0, 1, DisplayName = "Back_Az180_El30_Q0_TopRight")]`. + * 更改摘要:调整了特定测试用例的期望象限以匹配代码在180度方位角简化处理逻辑下的行为。 + * 原因:用户选择方案1处理剩余的测试失败。 + * 阻碍:无 + * 用户确认状态:[成功 - 基于最终测试结果推断] +* [2024-07-30 10:00:04] + * 步骤:[1. 更新任务文件 RcsPattern_TestFixes.md 以包含坐标系讨论的摘要。] + * 修改: + * `RcsPattern_TestFixes.md`: + * 追加了 ## 关于坐标系的讨论 部分,总结了RcsPattern类与坐标系(局部与世界)相关的行为和依赖关系。 + * 更改摘要:记录了用户关于坐标系问题的提问以及对此的分析结论。 + * 原因:执行计划步骤 [1] + * 阻碍:无 + * 用户确认状态:[待确认] + +# 最终审查 (由 REVIEW 模式填充) +实施与最终计划(包含用户选择的方案1来调整测试用例)完全匹配。具体如下: +1. **`GetRcsValueDbSm(double azimuthDeg, double elevationDeg)` 方法修改**: Top/Bottom 视角中针对 `-90` 度方位角的象限条件调整正确,符合计划,对应测试已通过。 +2. **`GetRcsValueDbSm(Vector3D ...)` 方法修改**: Top/Bottom 视角中 `isLocallyTopQuadrant` 的计算已修正为使用正确的 `localUp` 向量,符合计划,对应测试已通过。 +3. **测试用例 `Back_Az180_El30_Q0_TopRight` 修改**: 测试期望值已按照用户选择的方案1从 `0` 调整为 `1`,符合计划,对应测试已通过。 + +所有先前失败的测试现在均已通过。未发现与计划存在未报告的偏差。 + +**针对坐标系问题的补充审查 (2024-07-30):** +用户提出的关于坐标系使用的问题(RcsPattern 是否使用世界坐标系)已得到解答。分析表明 `RcsPattern` 在目标局部坐标系下工作,不直接依赖世界坐标系。相应的分析和结论已作为补充信息记录在本文档的 `## 关于坐标系的讨论` 部分。此文档更新操作符合计划,内容准确。没有引入新的代码更改或偏差。 + +## 关于坐标系的讨论 (用户提问 - 2024-07-30) + +用户提问:项目使用的坐标系是右手坐标系,+Y 轴向上。RCS 处理的过程中,有使用到世界坐标系吗? + +**结论与分析摘要:** + +1. **`RcsPattern` 类主要在目标自身(局部)坐标系下运作。** 它不直接与外部的世界坐标系交互或依赖其特定定义(如右手,+Y向上)。 +2. 向量输入方法 `GetRcsValueDbSm(Vector3D observerDirectionInTargetFrame, Vector3D targetForwardVectorInTargetFrame, ...)` 的参数(如 `observerDirectionInTargetFrame` 和目标轴向量)明确要求是在**目标局部坐标系**中定义的。 +3. 角度输入方法 `GetRcsValueDbSm(double azimuthDeg, double elevationDeg)` 中的方位角和俯仰角也是相对于**目标自身的参考轴**(如"全局前方参考线"和"全局水平面")定义的,这里的"全局"是指目标级的参考,而非世界坐标系。 +4. **世界坐标系的角色**:调用 `RcsPattern` 的外部代码(即您的项目代码)负责处理从世界坐标系到目标局部坐标系的转换。这包括: + * 确定目标在世界坐标系中的姿态(旋转)。 + * 计算观察者相对于目标的方向(可能先在世界坐标系中计算)。 + * 将此观察方向向量从世界坐标系转换到目标局部坐标系中,然后传递给 `RcsPattern`。 + * 定义目标自身的 `Forward`、`Up`、`Right` 向量时,需要考虑目标模型本身的轴定义以及其在世界中的姿态,最终得到在目标局部坐标系下表示这些轴的向量(通常是单位向量,如 `(1,0,0)` 等,具体取决于局部坐标系的选取)。 +5. **项目坐标系约定 (+Y 向上)**:这个约定主要影响您的项目代码如何进行上述的世界坐标到目标局部坐标的转换,以及如何定义目标的标准姿态或局部轴向。`RcsPattern` 类本身会正确处理您以其参数形式提供的任何(有效的)目标局部坐标系定义。 +6. **无需修改 `RcsPattern`**:基于以上分析,`RcsPattern` 类的当前设计是合理的,不需要修改来"适应"特定的世界坐标系。关键在于调用方正确提供目标局部坐标系下的输入。 \ No newline at end of file diff --git a/ThreatSource.Tests/Utils/Vector3DPerformanceTests.cs b/ThreatSource.Tests/Utils/Vector3DPerformanceTests.cs new file mode 100644 index 0000000..e8be516 --- /dev/null +++ b/ThreatSource.Tests/Utils/Vector3DPerformanceTests.cs @@ -0,0 +1,147 @@ +using Microsoft.VisualStudio.TestTools.UnitTesting; +using System.Diagnostics; +using ThreatSource.Utils; // Assuming Vector3D is here, adjust if necessary +using System.Collections.Generic; +using System; + +namespace ThreatSource.Tests.Utils +{ + [TestClass] + public class Vector3DPerformanceTests + { + private const int Iterations = 1000000; // Number of iterations for performance testing + + private Vector3D CreateVector(double i) + { + // Helper to create slightly different vectors + return new Vector3D(i, i + 1.5, i * 0.8); + } + + [TestMethod] + public void Baseline_Vector3D_Class_Performance() + { + // This test assumes Vector3D is currently a CLASS. + // We will run this once, record results, then change Vector3D to struct and run again. + + Stopwatch stopwatch = new Stopwatch(); + List vectors = new List(Iterations); + double totalMagnitude = 0; + Random rand = new Random(123); // Seeded for reproducibility + + // --- Test 1: Creation Performance --- + stopwatch.Start(); + for (int i = 0; i < Iterations; i++) + { + vectors.Add(CreateVector(i * rand.NextDouble())); + } + stopwatch.Stop(); + Console.WriteLine($"[Baseline Class] Creation of {Iterations} Vector3D instances: {stopwatch.ElapsedMilliseconds} ms"); + long creationTime = stopwatch.ElapsedMilliseconds; + + // Ensure vectors are used to prevent over-optimization by compiler + if (vectors.Count == 0) throw new Exception("Vector list empty after creation."); + + Vector3D tempVector1 = CreateVector(10.1); + Vector3D tempVector2 = CreateVector(20.2); + + // --- Test 2: Addition Performance --- + stopwatch.Restart(); + for (int i = 0; i < Iterations; i++) + { + tempVector1 = tempVector1 + vectors[i]; + } + stopwatch.Stop(); + Console.WriteLine($"[Baseline Class] {Iterations} Vector3D Additions: {stopwatch.ElapsedMilliseconds} ms"); + long additionTime = stopwatch.ElapsedMilliseconds; + + // --- Test 3: Subtraction Performance --- + stopwatch.Restart(); + for (int i = 0; i < Iterations; i++) + { + tempVector1 = tempVector1 - vectors[i]; + } + stopwatch.Stop(); + Console.WriteLine($"[Baseline Class] {Iterations} Vector3D Subtractions: {stopwatch.ElapsedMilliseconds} ms"); + long subtractionTime = stopwatch.ElapsedMilliseconds; + + // --- Test 4: Scalar Multiplication Performance --- + stopwatch.Restart(); + for (int i = 0; i < Iterations; i++) + { + tempVector1 = vectors[i] * 2.5; + } + stopwatch.Stop(); + Console.WriteLine($"[Baseline Class] {Iterations} Vector3D Scalar Multiplications: {stopwatch.ElapsedMilliseconds} ms"); + long scalarMultTime = stopwatch.ElapsedMilliseconds; + + // --- Test 5: Dot Product Performance --- + double dotProductSum = 0; + stopwatch.Restart(); + for (int i = 0; i < Iterations; i++) + { + dotProductSum += Vector3D.DotProduct(vectors[i], tempVector2); + } + stopwatch.Stop(); + Console.WriteLine($"[Baseline Class] {Iterations} Vector3D Dot Products: {stopwatch.ElapsedMilliseconds} ms"); + long dotProductTime = stopwatch.ElapsedMilliseconds; + + // --- Test 6: Cross Product Performance --- + stopwatch.Restart(); + for (int i = 0; i < Iterations; i++) + { + tempVector1 = Vector3D.CrossProduct(vectors[i], tempVector2); + } + stopwatch.Stop(); + Console.WriteLine($"[Baseline Class] {Iterations} Vector3D Cross Products: {stopwatch.ElapsedMilliseconds} ms"); + long crossProductTime = stopwatch.ElapsedMilliseconds; + + // --- Test 7: Magnitude Performance --- + stopwatch.Restart(); + for (int i = 0; i < Iterations; i++) + { + totalMagnitude += vectors[i].Magnitude(); + } + stopwatch.Stop(); + Console.WriteLine($"[Baseline Class] {Iterations} Vector3D Magnitudes: {stopwatch.ElapsedMilliseconds} ms"); + long magnitudeTime = stopwatch.ElapsedMilliseconds; + + // --- Test 8: Normalization Performance --- + stopwatch.Restart(); + for (int i = 0; i < Iterations; i++) + { + // Avoid normalizing zero vectors which might throw exceptions or return NaN + if (vectors[i].MagnitudeSquared() > 0.00001) + { + tempVector1 = vectors[i].Normalize(); + } + else + { + tempVector1 = vectors[i]; // Assign to ensure work is done + } + } + stopwatch.Stop(); + Console.WriteLine($"[Baseline Class] {Iterations} Vector3D Normalizations: {stopwatch.ElapsedMilliseconds} ms"); + long normalizeTime = stopwatch.ElapsedMilliseconds; + + Console.WriteLine($"[Baseline Class] Total magnitude sum (to prevent optimization): {totalMagnitude}"); + Console.WriteLine($"[Baseline Class] Final tempVector1 (to prevent optimization): {tempVector1}"); + Console.WriteLine($"[Baseline Class] Final dotProductSum (to prevent optimization): {dotProductSum}"); + + // For MSTest, Assert.Inconclusive can be used to output results without failing the test. + // Alternatively, a simple Console.WriteLine at the end of the test with a summary is also fine + // if the test isn't meant to pass/fail based on performance but just to gather data. + // For this baseline, we'll use Assert.Inconclusive to clearly mark the output in test results. + string resultSummary = $"Baseline performance (Vector3D as CLASS):\n" + + $"Creation: {creationTime} ms\n" + + $"Addition: {additionTime} ms\n" + + $"Subtraction: {subtractionTime} ms\n" + + $"Scalar Mult: {scalarMultTime} ms\n" + + $"Dot Product: {dotProductTime} ms\n" + + $"Cross Product: {crossProductTime} ms\n" + + $"Magnitude: {magnitudeTime} ms\n" + + $"Normalization: {normalizeTime} ms"; + Console.WriteLine(resultSummary); // Ensure it's also printed to console output for easy viewing + Assert.Inconclusive(resultSummary); // Marks test as inconclusive and shows message in test explorer + } + } +} \ No newline at end of file diff --git a/ThreatSource.Tests/src/Equipment/RcsPatternTests.cs b/ThreatSource.Tests/src/Equipment/RcsPatternTests.cs new file mode 100644 index 0000000..362186a --- /dev/null +++ b/ThreatSource.Tests/src/Equipment/RcsPatternTests.cs @@ -0,0 +1,584 @@ +using Microsoft.VisualStudio.TestTools.UnitTesting; +using ThreatSource.Equipment; +using ThreatSource.Utils; // For Vector3D +using System; +using System.Collections.Generic; // Required for List if RcsMatrixDataSource is directly checked +using System.Linq; // Required for some LINQ operations if used in tests + +namespace ThreatSource.Tests.Equipment +{ + [TestClass] + public class RcsPatternTests + { + // This should ideally match the constant in RcsPattern.cs or be sourced from it. + // For testing, we define it here. Ensure it's consistent if RcsPattern.DEFAULT_RCS_DBSM changes. + private const double DEFAULT_RCS_DBSM_IN_PATTERN = -20.0; + private const double TEST_DELTA = 1e-6; // Tolerance for floating-point comparisons + + /// + /// Helper method to create a test RCS matrix with predictable values. + /// + /// Number of rows for the matrix. + /// Number of columns for the matrix. + /// Function to generate values. If null, uses (i * 10 + j + 1.0). + /// A 2D double array representing the RCS matrix. + private double[,] CreateTestMatrix(int rows = 6, int cols = 4, Func? valueFunc = null) + { + var matrix = new double[rows, cols]; + if (valueFunc == null) + { + valueFunc = (r, c) => (r * 10 + c + 1.0); // Default: e.g., R0C0=1.0, R0C1=2.0,... R1C0=11.0 + } + + for (int i = 0; i < rows; i++) + { + for (int j = 0; j < cols; j++) + { + matrix[i, j] = valueFunc(i, j); + } + } + return matrix; + } + + // Test methods will be added here + + [TestMethod] + public void DefaultConstructor_InitializesCorrectly() + { + // Arrange & Act + var rcsPattern = new RcsPattern(); + + // Assert + // Check RcsMatrixDataSource (List>) + Assert.IsNotNull(rcsPattern.RcsMatrixDataSource, "RcsMatrixDataSource should not be null."); + Assert.AreEqual(6, rcsPattern.RcsMatrixDataSource.Count, "RcsMatrixDataSource should have 6 rows."); + for (int i = 0; i < 6; i++) + { + Assert.IsNotNull(rcsPattern.RcsMatrixDataSource[i], $"RcsMatrixDataSource row {i} should not be null."); + Assert.AreEqual(4, rcsPattern.RcsMatrixDataSource[i].Count, $"RcsMatrixDataSource row {i} should have 4 columns."); + for (int j = 0; j < 4; j++) + { + Assert.AreEqual(DEFAULT_RCS_DBSM_IN_PATTERN, rcsPattern.RcsMatrixDataSource[i][j], TEST_DELTA, $"RcsMatrixDataSource[{i}][{j}] should be default value."); + } + } + + // Check RcsMatrixData (double[,]) + var rcsData = rcsPattern.RcsMatrixData; + Assert.IsNotNull(rcsData, "RcsMatrixData should not be null."); + Assert.AreEqual(6, rcsData.GetLength(0), "RcsMatrixData should have 6 rows."); + Assert.AreEqual(4, rcsData.GetLength(1), "RcsMatrixData should have 4 columns."); + for (int i = 0; i < 6; i++) + { + for (int j = 0; j < 4; j++) + { + Assert.AreEqual(DEFAULT_RCS_DBSM_IN_PATTERN, rcsData[i, j], TEST_DELTA, $"RcsMatrixData[{i},{j}] should be default value."); + } + } + } + + [TestMethod] + public void ParameterizedConstructor_WithValidMatrix_InitializesCorrectly() + { + // Arrange + double[,] expectedMatrix = CreateTestMatrix(); // Uses default 6x4 with (i*10+j+1) values + + // Act + var rcsPattern = new RcsPattern(expectedMatrix); + var actualMatrixData = rcsPattern.RcsMatrixData; + var actualMatrixDataSource = rcsPattern.RcsMatrixDataSource; + + // Assert + // Check RcsMatrixData (double[,]) + Assert.AreEqual(6, actualMatrixData.GetLength(0)); + Assert.AreEqual(4, actualMatrixData.GetLength(1)); + for (int i = 0; i < 6; i++) + { + for (int j = 0; j < 4; j++) + { + Assert.AreEqual(expectedMatrix[i, j], actualMatrixData[i, j], TEST_DELTA, $"RcsMatrixData[{i},{j}] did not match expected."); + } + } + + // Check RcsMatrixDataSource (List>) + Assert.IsNotNull(actualMatrixDataSource); + Assert.AreEqual(6, actualMatrixDataSource.Count); + for (int i = 0; i < 6; i++) + { + Assert.IsNotNull(actualMatrixDataSource[i]); + Assert.AreEqual(4, actualMatrixDataSource[i].Count); + for (int j = 0; j < 4; j++) + { + Assert.AreEqual(expectedMatrix[i, j], actualMatrixDataSource[i][j], TEST_DELTA, $"RcsMatrixDataSource[{i}][{j}] did not match expected."); + } + } + } + + [TestMethod] + public void ParameterizedConstructor_WithNullMatrix_ThrowsArgumentNullException() + { + // Arrange + double[,]? nullMatrix = null; + + // Act & Assert + Assert.ThrowsException(() => new RcsPattern(nullMatrix!)); + } + + [TestMethod] + public void ParameterizedConstructor_WithInvalidDimensions_ThrowsArgumentException() + { + // Arrange + double[,] wrongRowsMatrix = CreateTestMatrix(5, 4); + double[,] wrongColsMatrix = CreateTestMatrix(6, 3); + + // Act & Assert + Assert.ThrowsException(() => new RcsPattern(wrongRowsMatrix), "Constructor should throw for wrong row count."); + Assert.ThrowsException(() => new RcsPattern(wrongColsMatrix), "Constructor should throw for wrong column count."); + } + + [TestMethod] + public void RcsMatrixData_HandlesInvalidDataSource_ReturnsDefault() + { + // Arrange + var rcsPattern = new RcsPattern(); // Starts with a valid default + var defaultMatrix = CreateTestMatrix(6, 4, (r,c) => DEFAULT_RCS_DBSM_IN_PATTERN); + + // Act & Assert Case 1: DataSource is null + rcsPattern.RcsMatrixDataSource = null!; + var rcsDataAfterNullSource = rcsPattern.RcsMatrixData; + CollectionAssert.AreEqual(defaultMatrix, rcsDataAfterNullSource, "RcsMatrixData should return default when DataSource is null."); + + // Act & Assert Case 2: DataSource has wrong number of rows + rcsPattern.RcsMatrixDataSource = [[1, 2, 3, 4]]; // Only 1 row + var rcsDataAfterWrongRowCount = rcsPattern.RcsMatrixData; + CollectionAssert.AreEqual(defaultMatrix, rcsDataAfterWrongRowCount, "RcsMatrixData should return default for wrong row count in DataSource."); + + // Act & Assert Case 3: A row in DataSource is null + rcsPattern = new RcsPattern(); // Reset to valid + rcsPattern.RcsMatrixDataSource[2] = null!; // Set one row to null + var rcsDataAfterNullRow = rcsPattern.RcsMatrixData; + // RcsPattern.ConvertSourceToGrid fills the specific bad row with defaults, others remain. + // So, we need to construct the specific expected matrix for this case. + var expectedMatrixWithOneDefaultRow = new RcsPattern().RcsMatrixData; // Start with all defaults + // The original test matrix for RcsPattern() IS the default matrix. So no change needed for this specific setup. + // If RcsPattern() initialized differently, we would need to adjust 'expectedMatrixWithOneDefaultRow' accordingly. + CollectionAssert.AreEqual(expectedMatrixWithOneDefaultRow, rcsDataAfterNullRow, "RcsMatrixData should handle null row in DataSource by defaulting that row."); + + // Act & Assert Case 4: A row in DataSource has wrong number of columns + rcsPattern = new RcsPattern(); // Reset to valid + rcsPattern.RcsMatrixDataSource[3] = [1, 2, 3]; // Row 3 has only 3 columns + var rcsDataAfterWrongColCountInRow = rcsPattern.RcsMatrixData; + // Similar to case 3, RcsPattern.ConvertSourceToGrid should default the problematic row. + var expectedMatrixWithOneDefaultRowAgain = new RcsPattern().RcsMatrixData; + CollectionAssert.AreEqual(expectedMatrixWithOneDefaultRowAgain, rcsDataAfterWrongColCountInRow, "RcsMatrixData should handle wrong column count in a DataSource row by defaulting that row."); + } + + // Tests for GetRcsValueDbSm(double azimuthDeg, double elevationDeg) + // Helper for angle-based tests, uses a predictable matrix + private RcsPattern CreateAngleTestPattern() + { + // Matrix where RcsMatrix[row, col] = (row + 1) * 100 + (col + 1) * 10 for easy identification + return new RcsPattern(CreateTestMatrix(6, 4, (r, c) => (r + 1) * 100 + (c + 1) * 10)); + } + + [DataTestMethod] + [DataRow(0.0, 0, DisplayName = "Top_Az0_Q0")] // El=75, Az=0 -> Top, Q0 (Right-Top for Top view) + [DataRow(45.0, 0, DisplayName = "Top_Az45_Q0")] // El=75, Az=45 -> Top, Q0 + [DataRow(89.9, 0, DisplayName = "Top_Az89.9_Q0")] // El=75, Az=89.9 -> Top, Q0 + [DataRow(90.0, 1, DisplayName = "Top_Az90_Q1")] // El=75, Az=90 -> Top, Q1 (Left-Top for Top view) + [DataRow(135.0, 1, DisplayName = "Top_Az135_Q1")] // El=75, Az=135 -> Top, Q1 + [DataRow(179.9, 1, DisplayName = "Top_Az179.9_Q1")] // El=75, Az=179.9 -> Top, Q1 + [DataRow(-0.1, 3, DisplayName = "Top_Az-0.1_Q3")] // El=75, Az=-0.1 -> Top, Q3 (Right-Bottom for Top view) + [DataRow(-45.0, 3, DisplayName = "Top_Az-45_Q3")] // El=75, Az=-45 -> Top, Q3 + [DataRow(-89.9, 3, DisplayName = "Top_Az-89.9_Q3")] // El=75, Az=-89.9 -> Top, Q3 + [DataRow(-90.0, 2, DisplayName = "Top_Az-90_Q2")] // El=75, Az=-90 -> Top, Q2 (Left-Bottom for Top view) + [DataRow(-135.0, 2, DisplayName = "Top_Az-135_Q2")] // El=75, Az=-135 -> Top, Q2 + [DataRow(-179.9, 2, DisplayName = "Top_Az-179.9_Q2")]// El=75, Az=-179.9 -> Top, Q2 + [DataRow(180.0, 2, DisplayName = "Top_Az180_Q2")] // Az 180 is equivalent to -180 for quadrant logic here (becomes Q2) + [DataRow(-180.0, 2, DisplayName = "Top_Az-180_Q2")] + public void GetRcsAngleBased_TopAspect_ReturnsCorrectQuadrantValue(double azimuthDeg, int expectedQuadrantColumn) + { + var rcsPattern = CreateAngleTestPattern(); + double elevationDeg = 75.0; // Top aspect + int expectedMajorAspectRow = 4; // Top + double expectedValue = (expectedMajorAspectRow + 1) * 100 + (expectedQuadrantColumn + 1) * 10; + + double actualValue = rcsPattern.GetRcsValueDbSm(azimuthDeg, elevationDeg); + + Assert.AreEqual(expectedValue, actualValue, TEST_DELTA, $"Failed for Az: {azimuthDeg}, El: {elevationDeg}. Expected Row {expectedMajorAspectRow}, Col {expectedQuadrantColumn}."); + } + + [DataTestMethod] + // Similar DataRows as TopAspect, just for elevation -75 + [DataRow(0.0, 0, DisplayName = "Bottom_Az0_Q0")] + [DataRow(89.9, 0, DisplayName = "Bottom_Az89.9_Q0")] + [DataRow(90.0, 1, DisplayName = "Bottom_Az90_Q1")] + [DataRow(179.9, 1, DisplayName = "Bottom_Az179.9_Q1")] + [DataRow(-0.1, 3, DisplayName = "Bottom_Az-0.1_Q3")] + [DataRow(-89.9, 3, DisplayName = "Bottom_Az-89.9_Q3")] + [DataRow(-90.0, 2, DisplayName = "Bottom_Az-90_Q2")] + [DataRow(-179.9, 2, DisplayName = "Bottom_Az-179.9_Q2")] + [DataRow(180.0, 2, DisplayName = "Bottom_Az180_Q2")] + [DataRow(-180.0, 2, DisplayName = "Bottom_Az-180_Q2")] + public void GetRcsAngleBased_BottomAspect_ReturnsCorrectQuadrantValue(double azimuthDeg, int expectedQuadrantColumn) + { + var rcsPattern = CreateAngleTestPattern(); + double elevationDeg = -75.0; // Bottom aspect + int expectedMajorAspectRow = 5; // Bottom + double expectedValue = (expectedMajorAspectRow + 1) * 100 + (expectedQuadrantColumn + 1) * 10; + + double actualValue = rcsPattern.GetRcsValueDbSm(azimuthDeg, elevationDeg); + + Assert.AreEqual(expectedValue, actualValue, TEST_DELTA, $"Failed for Az: {azimuthDeg}, El: {elevationDeg}. Expected Row {expectedMajorAspectRow}, Col {expectedQuadrantColumn}."); + } + + // Front Aspect (Row 0) + [DataTestMethod] + [DataRow(0.0, 30.0, 0, DisplayName = "Front_Az0_El30_Q0_TopRight")] // Az 0, El > 0 -> Front, Q0 (Right-Top) + [DataRow(44.9, 30.0, 0, DisplayName = "Front_Az44.9_El30_Q0_TopRight")] // Az 44.9, El > 0 -> Front, Q0 + [DataRow(0.0, -30.0, 3, DisplayName = "Front_Az0_El-30_Q3_BottomRight")] // Az 0, El < 0 -> Front, Q3 (Right-Bottom) + [DataRow(44.9, -30.0, 3, DisplayName = "Front_Az44.9_El-30_Q3_BottomRight")]// Az 44.9, El < 0 -> Front, Q3 + [DataRow(-0.1, 30.0, 1, DisplayName = "Front_Az-0.1_El30_Q1_TopLeft")] // Az -0.1, El > 0 -> Front, Q1 (Left-Top) + [DataRow(-44.9, 30.0, 1, DisplayName = "Front_Az-44.9_El30_Q1_TopLeft")] // Az -44.9, El > 0 -> Front, Q1 + [DataRow(-0.1, -30.0, 2, DisplayName = "Front_Az-0.1_El-30_Q2_BottomLeft")] // Az -0.1, El < 0 -> Front, Q2 (Left-Bottom) + [DataRow(-44.9, -30.0, 2, DisplayName = "Front_Az-44.9_El-30_Q2_BottomLeft")]// Az -44.9, El < 0 -> Front, Q2 + public void GetRcsAngleBased_FrontAspect_ReturnsCorrectQuadrantValue(double azimuthDeg, double elevationDeg, int expectedQuadrantColumn) + { + var rcsPattern = CreateAngleTestPattern(); + int expectedMajorAspectRow = 0; // Front + double expectedValue = (expectedMajorAspectRow + 1) * 100 + (expectedQuadrantColumn + 1) * 10; + double actualValue = rcsPattern.GetRcsValueDbSm(azimuthDeg, elevationDeg); + Assert.AreEqual(expectedValue, actualValue, TEST_DELTA, $"Front Aspect - Failed for Az: {azimuthDeg}, El: {elevationDeg}."); + } + + // Back Aspect (Row 1) - Azimuth for back is 135 to -135 (or 135 to 225) + [DataTestMethod] + [DataRow(179.9, 30.0, 0, DisplayName = "Back_Az179.9_El30_Q0_TopRight")] // Az 179.9 (>0), El > 0 -> Back, Q0 (Simplified logic) + [DataRow(135.1, 30.0, 0, DisplayName = "Back_Az135.1_El30_Q0_TopRight")] // Az 135.1 (>0), El > 0 -> Back, Q0 + [DataRow(179.9, -30.0, 3, DisplayName = "Back_Az179.9_El-30_Q3_BottomRight")]// Az 179.9 (>0), El < 0 -> Back, Q3 + [DataRow(135.1, -30.0, 3, DisplayName = "Back_Az135.1_El-30_Q3_BottomRight")]// Az 135.1 (>0), El < 0 -> Back, Q3 + [DataRow(-179.9, 30.0, 1, DisplayName = "Back_Az-179.9_El30_Q1_TopLeft")] // Az -179.9 (<0), El > 0 -> Back, Q1 + [DataRow(-135.1, 30.0, 1, DisplayName = "Back_Az-135.1_El30_Q1_TopLeft")] // Az -135.1 (<0), El > 0 -> Back, Q1 + [DataRow(-179.9, -30.0, 2, DisplayName = "Back_Az-179.9_El-30_Q2_BottomLeft")]// Az -179.9 (<0), El < 0 -> Back, Q2 + [DataRow(-135.1, -30.0, 2, DisplayName = "Back_Az-135.1_El-30_Q2_BottomLeft")]// Az -135.1 (<0), El < 0 -> Back, Q2 + [DataRow(180.0, 30.0, 1, DisplayName = "Back_Az180_El30_Q0_TopRight")] // Az 180.0, El > 0 -> Back, Q0 (Simplified logic) + [DataRow(-180.0, 30.0, 1, DisplayName = "Back_Az-180_El30_Q1_TopLeft")]// Az -180.0, El > 0 -> Back, Q1 (Simplified logic) + public void GetRcsAngleBased_BackAspect_ReturnsCorrectQuadrantValue(double azimuthDeg, double elevationDeg, int expectedQuadrantColumn) + { + var rcsPattern = CreateAngleTestPattern(); + int expectedMajorAspectRow = 1; // Back + double expectedValue = (expectedMajorAspectRow + 1) * 100 + (expectedQuadrantColumn + 1) * 10; + double actualValue = rcsPattern.GetRcsValueDbSm(azimuthDeg, elevationDeg); + Assert.AreEqual(expectedValue, actualValue, TEST_DELTA, $"Back Aspect - Failed for Az: {azimuthDeg}, El: {elevationDeg}."); + } + + // Right Aspect (Row 3) - Azimuth for right is 45 to 135 + [DataTestMethod] + [DataRow(45.1, 30.0, 0, DisplayName = "Right_Az45.1_El30_Q0_TopRight")] // Az 45.1 (<90), El > 0 -> Right, Q0 + [DataRow(89.9, 30.0, 0, DisplayName = "Right_Az89.9_El30_Q0_TopRight")] // Az 89.9 (<90), El > 0 -> Right, Q0 + [DataRow(45.1, -30.0, 3, DisplayName = "Right_Az45.1_El-30_Q3_BottomRight")]// Az 45.1 (<90), El < 0 -> Right, Q3 + [DataRow(89.9, -30.0, 3, DisplayName = "Right_Az89.9_El-30_Q3_BottomRight")]// Az 89.9 (<90), El < 0 -> Right, Q3 + [DataRow(90.1, 30.0, 1, DisplayName = "Right_Az90.1_El30_Q1_TopLeft")] // Az 90.1 (>=90), El > 0 -> Right, Q1 + [DataRow(134.9, 30.0, 1, DisplayName = "Right_Az134.9_El30_Q1_TopLeft")] // Az 134.9 (>=90), El > 0 -> Right, Q1 + [DataRow(90.1, -30.0, 2, DisplayName = "Right_Az90.1_El-30_Q2_BottomLeft")] // Az 90.1 (>=90), El < 0 -> Right, Q2 + [DataRow(134.9, -30.0, 2, DisplayName = "Right_Az134.9_El-30_Q2_BottomLeft")]// Az 134.9 (>=90), El < 0 -> Right, Q2 + public void GetRcsAngleBased_RightAspect_ReturnsCorrectQuadrantValue(double azimuthDeg, double elevationDeg, int expectedQuadrantColumn) + { + var rcsPattern = CreateAngleTestPattern(); + int expectedMajorAspectRow = 3; // Right + double expectedValue = (expectedMajorAspectRow + 1) * 100 + (expectedQuadrantColumn + 1) * 10; + double actualValue = rcsPattern.GetRcsValueDbSm(azimuthDeg, elevationDeg); + Assert.AreEqual(expectedValue, actualValue, TEST_DELTA, $"Right Aspect - Failed for Az: {azimuthDeg}, El: {elevationDeg}."); + } + + // Left Aspect (Row 2) - Azimuth for left is -45 to -135 + [DataTestMethod] + [DataRow(-45.1, 30.0, 1, DisplayName = "Left_Az-45.1_El30_Q1_TopLeft")] // Az -45.1 (not < -90), El > 0 -> Left, Q1 + [DataRow(-89.9, 30.0, 1, DisplayName = "Left_Az-89.9_El30_Q1_TopLeft")] // Az -89.9 (not < -90), El > 0 -> Left, Q1 + [DataRow(-45.1, -30.0, 2, DisplayName = "Left_Az-45.1_El-30_Q2_BottomLeft")]// Az -45.1 (not < -90), El < 0 -> Left, Q2 + [DataRow(-89.9, -30.0, 2, DisplayName = "Left_Az-89.9_El-30_Q2_BottomLeft")]// Az -89.9 (not < -90), El < 0 -> Left, Q2 + [DataRow(-90.1, 30.0, 0, DisplayName = "Left_Az-90.1_El30_Q0_TopRight")] // Az -90.1 (< -90), El > 0 -> Left, Q0 + [DataRow(-134.9, 30.0, 0, DisplayName = "Left_Az-134.9_El30_Q0_TopRight")] // Az -134.9 (< -90), El > 0 -> Left, Q0 + [DataRow(-90.1, -30.0, 3, DisplayName = "Left_Az-90.1_El-30_Q3_BottomRight")]// Az -90.1 (< -90), El < 0 -> Left, Q3 + [DataRow(-134.9, -30.0, 3, DisplayName = "Left_Az-134.9_El-30_Q3_BottomRight")]// Az -134.9 (< -90), El < 0 -> Left, Q3 + public void GetRcsAngleBased_LeftAspect_ReturnsCorrectQuadrantValue(double azimuthDeg, double elevationDeg, int expectedQuadrantColumn) + { + var rcsPattern = CreateAngleTestPattern(); + int expectedMajorAspectRow = 2; // Left + double expectedValue = (expectedMajorAspectRow + 1) * 100 + (expectedQuadrantColumn + 1) * 10; + double actualValue = rcsPattern.GetRcsValueDbSm(azimuthDeg, elevationDeg); + Assert.AreEqual(expectedValue, actualValue, TEST_DELTA, $"Left Aspect - Failed for Az: {azimuthDeg}, El: {elevationDeg}."); + } + + [TestMethod] + public void GetRcsAngleBased_InvalidAngles_ReturnsDefaultRcs() + { + var rcsPattern = CreateAngleTestPattern(); // Content doesn't matter, expecting default + + // Elevation too high/low (should be clamped by method, but good to test boundary behavior of logic) + // The method's internal logic for row selection based on elevation > 70 or < -70 is robust. + // This test is more about extreme azimuths with valid elevations. + Assert.AreEqual(rcsPattern.GetRcsValueDbSm(720, 30), rcsPattern.GetRcsValueDbSm(0, 30), TEST_DELTA, "Azimuth 720 should be same as 0"); + Assert.AreEqual(rcsPattern.GetRcsValueDbSm(-720, 30), rcsPattern.GetRcsValueDbSm(0, 30), TEST_DELTA, "Azimuth -720 should be same as 0"); + + // Test clamping of elevation + // If elevation is 95, it's clamped to 90. Row 4 (Top). Azimuth 0 -> Col 0. + double expectedTopQ0 = (4 + 1) * 100 + (0 + 1) * 10; + Assert.AreEqual(expectedTopQ0, rcsPattern.GetRcsValueDbSm(0, 95), TEST_DELTA, "Elevation 95 should be clamped to 90, Top Q0."); + + // If elevation is -95, it's clamped to -90. Row 5 (Bottom). Azimuth 0 -> Col 0. + double expectedBottomQ0 = (5 + 1) * 100 + (0 + 1) * 10; + Assert.AreEqual(expectedBottomQ0, rcsPattern.GetRcsValueDbSm(0, -95), TEST_DELTA, "Elevation -95 should be clamped to -90, Bottom Q0."); + + // Test if any unforeseen combination leads to the debug path returning DEFAULT_RCS_DBSM_IN_PATTERN + // This requires finding a case where majorAspectRow or quadrantColumn are out of bounds after logic. + // Given current logic, it's hard to hit the final 'else' that returns DEFAULT_RCS_DBSM_IN_PATTERN + // because azimuth and elevation normalization + selection logic seems to cover all inputs. + // We can however test the specific case where the underlying matrix might be default if not for CreateAngleTestPattern + var defaultPattern = new RcsPattern(); // All values are DEFAULT_RCS_DBSM_IN_PATTERN + // If GetRcsValueDbSm somehow failed to find a cell and returned DEFAULT_RCS_DBSM_IN_PATTERN from its final else, + // this would pass. This isn't a strong test for that path without more specific conditions. + // For now, we assume the existing DataRow tests cover valid cell selection. + // A more robust test for the fallback would be if matrix lookup itself failed, which is not what this method tests. + } + + // Tests for GetRcsValueDbSm(Vector3D observerDirection, ...) + private readonly Vector3D TARGET_FORWARD = new Vector3D(1, 0, 0); + private readonly Vector3D TARGET_UP = new Vector3D(0, 1, 0); + private readonly Vector3D TARGET_RIGHT = new Vector3D(0, 0, 1); + + // Top Aspect (Row 4) - Observer high above + [DataTestMethod] + // Azimuths relative to target forward (X-axis), projected onto target's XZ plane (when view is Top) + // For Top view, localRight is targetRight (Z), localUp is -targetForward (-X) + // Q0 (LocalRight, LocalUp) -> Z>0, -X>0 => Z>0, X<0. (e.g. observer at (-1, 10, 1) -> Q0) + // Q1 (LocalLeft, LocalUp) -> -Z>0, -X>0 => Z<0, X<0. (e.g. observer at (-1, 10, -1) -> Q1) + // Q2 (LocalLeft, LocalDown) -> -Z>0, X>0 => Z<0, X>0. (e.g. observer at (1, 10, -1) -> Q2) + // Q3 (LocalRight, LocalDown) -> Z>0, X>0 => Z>0, X>0. (e.g. observer at (1, 10, 1) -> Q3) + [DataRow(-0.1, 10.0, 0.1, 0, DisplayName = "VectorTop_NearFwdSlightRight_Q0")] // X<0 (localUp), Z>0 (localRight) -> Q0 (Obs: -0.1, 10, 0.1) + [DataRow(-1.0, 10.0, 1.0, 0, DisplayName = "VectorTop_FwdLeft_Q0")] // X<0, Z>0 -> Q0 + [DataRow(-1.0, 10.0, -1.0, 1, DisplayName = "VectorTop_FwdRight_Q1")]// X<0, Z<0 -> Q1 + [DataRow(1.0, 10.0, -1.0, 2, DisplayName = "VectorTop_BackRight_Q2")] // X>0, Z<0 -> Q2 + [DataRow(1.0, 10.0, 1.0, 3, DisplayName = "VectorTop_BackLeft_Q3")] // X>0, Z>0 -> Q3 + public void GetRcsVectorBased_TopAspect_ReturnsCorrectQuadrantValue(double obsX, double obsY, double obsZ, int expectedQuadrantColumn) + { + var rcsPattern = CreateAngleTestPattern(); + var observerDirection = new Vector3D(obsX, obsY, obsZ); + int expectedMajorAspectRow = 4; // Top + double expectedValue = (expectedMajorAspectRow + 1) * 100 + (expectedQuadrantColumn + 1) * 10; + + double actualValue = rcsPattern.GetRcsValueDbSm(observerDirection, TARGET_FORWARD, TARGET_UP, TARGET_RIGHT); + + Assert.AreEqual(expectedValue, actualValue, TEST_DELTA, $"Vector Top Aspect - Failed for Obs: {observerDirection}. Expected Row {expectedMajorAspectRow}, Col {expectedQuadrantColumn}."); + } + + // Bottom Aspect (Row 5) - Observer far below + [DataTestMethod] + // For Bottom view, localRight is targetRight (Z), localUp is targetForward (X) + // Q0 (LocalRight, LocalUp) -> Z>0, X>0. (e.g. observer at (1, -10, 1) -> Q0) + // Q1 (LocalLeft, LocalUp) -> -Z>0, X>0 => Z<0, X>0. (e.g. observer at (1, -10, -1) -> Q1) + // Q2 (LocalLeft, LocalDown) -> -Z>0, -X>0 => Z<0, X<0. (e.g. observer at (-1, -10, -1) -> Q2) + // Q3 (LocalRight, LocalDown) -> Z>0, -X>0 => Z>0, X<0. (e.g. observer at (-1, -10, 1) -> Q3) + [DataRow(1.0, -10.0, 1.0, 0, DisplayName = "VectorBottom_FwdLeft_Q0")] // X>0 (localUp), Z>0 (localRight) -> Q0 + [DataRow(1.0, -10.0, -1.0, 1, DisplayName = "VectorBottom_FwdRight_Q1")] // X>0, Z<0 -> Q1 + [DataRow(-1.0, -10.0, -1.0, 2, DisplayName = "VectorBottom_BackRight_Q2")]// X<0, Z<0 -> Q2 + [DataRow(-1.0, -10.0, 1.0, 3, DisplayName = "VectorBottom_BackLeft_Q3")] // X<0, Z>0 -> Q3 + public void GetRcsVectorBased_BottomAspect_ReturnsCorrectQuadrantValue(double obsX, double obsY, double obsZ, int expectedQuadrantColumn) + { + var rcsPattern = CreateAngleTestPattern(); + var observerDirection = new Vector3D(obsX, obsY, obsZ); + int expectedMajorAspectRow = 5; // Bottom + double expectedValue = (expectedMajorAspectRow + 1) * 100 + (expectedQuadrantColumn + 1) * 10; + + double actualValue = rcsPattern.GetRcsValueDbSm(observerDirection, TARGET_FORWARD, TARGET_UP, TARGET_RIGHT); + + Assert.AreEqual(expectedValue, actualValue, TEST_DELTA, $"Vector Bottom Aspect - Failed for Obs: {observerDirection}. Expected Row {expectedMajorAspectRow}, Col {expectedQuadrantColumn}."); + } + + // Front Aspect (Row 0) - Observer in front (positive X relative to target forward if TARGET_FORWARD is (1,0,0)) + // Global Azimuth near 0. observerDirection should have a large positive component along TARGET_FORWARD. + [DataTestMethod] + // For Front view (majorAspectRow 0), localRight is targetRight, localUp is targetUp. + // Q0 (LocalRight, LocalUp) -> dot(obs, targetRight) >= 0, dot(obs, targetUp) >= 0. + // If targetRight is Z-axis, targetUp is Y-axis: + // Q0: Z>=0, Y>=0. Example: obs=(10, 1, 1) + // Q1: Z<0, Y>=0. Example: obs=(10, 1, -1) + // Q2: Z<0, Y<0. Example: obs=(10, -1, -1) + // Q3: Z>=0, Y<0. Example: obs=(10, -1, 1) + [DataRow(10.0, 1.0, 1.0, 0, DisplayName = "VectorFront_UpRightQuadrant_Q0")] + [DataRow(10.0, 1.0, -1.0, 1, DisplayName = "VectorFront_UpLeftQuadrant_Q1")] + [DataRow(10.0, -1.0, -1.0, 2, DisplayName = "VectorFront_DownLeftQuadrant_Q2")] + [DataRow(10.0, -1.0, 1.0, 3, DisplayName = "VectorFront_DownRightQuadrant_Q3")] + // Test edge cases for dot products being zero + [DataRow(10.0, 0.0, 0.001, 0, DisplayName = "VectorFront_Y0_Zpos_Q0")] // Y=0, Z>0. dot(obs,Y)=0. isLocallyTopQuadrant=true. isLocallyRightQuadrant=true. -> Q0 + [DataRow(10.0, 0.0, -0.001, 1, DisplayName = "VectorFront_Y0_Zneg_Q1")]// Y=0, Z<0. isLocallyTopQuadrant=true. isLocallyRightQuadrant=false. -> Q1 + [DataRow(10.0, 0.001, 0.0, 0, DisplayName = "VectorFront_Ypos_Z0_Q0")] // Y>0, Z=0. isLocallyTopQuadrant=true. isLocallyRightQuadrant=true. -> Q0 + [DataRow(10.0, -0.001, 0.0, 3, DisplayName = "VectorFront_Yneg_Z0_Q3")]// Y<0, Z=0. isLocallyTopQuadrant=false. isLocallyRightQuadrant=true. -> Q3 + public void GetRcsVectorBased_FrontAspect_ReturnsCorrectQuadrantValue(double obsX, double obsY, double obsZ, int expectedQuadrantColumn) + { + var rcsPattern = CreateAngleTestPattern(); + var observerDirection = new Vector3D(obsX, obsY, obsZ); // Observer is mostly along positive X + int expectedMajorAspectRow = 0; // Front + double expectedValue = (expectedMajorAspectRow + 1) * 100 + (expectedQuadrantColumn + 1) * 10; + double actualValue = rcsPattern.GetRcsValueDbSm(observerDirection, TARGET_FORWARD, TARGET_UP, TARGET_RIGHT); + Assert.AreEqual(expectedValue, actualValue, TEST_DELTA, $"Vector Front Aspect - Failed for Obs: {observerDirection}."); + } + + // Back Aspect (Row 1) - Observer behind (negative X relative to target forward) + [DataTestMethod] + // For Back view (majorAspectRow 1), localRight is -targetRight, localUp is targetUp. + // Q0 (LocalRight, LocalUp) -> dot(obs, -targetRight) >= 0, dot(obs, targetUp) >= 0 => dot(obs, targetRight) <=0, dot(obs, targetUp) >=0 + // If targetRight is Z-axis, targetUp is Y-axis: + // Q0: Z<=0, Y>=0. Example: obs=(-10, 1, -1) + // Q1: Z>0, Y>=0. Example: obs=(-10, 1, 1) + // Q2: Z>0, Y<0. Example: obs=(-10, -1, 1) + // Q3: Z<=0, Y<0. Example: obs=(-10, -1, -1) + [DataRow(-10.0, 1.0, -1.0, 0, DisplayName = "VectorBack_UpLeftFacingTarget_Q0")] + [DataRow(-10.0, 1.0, 1.0, 1, DisplayName = "VectorBack_UpRightFacingTarget_Q1")] + [DataRow(-10.0, -1.0, 1.0, 2, DisplayName = "VectorBack_DownRightFacingTarget_Q2")] + [DataRow(-10.0, -1.0, -1.0, 3, DisplayName = "VectorBack_DownLeftFacingTarget_Q3")] + public void GetRcsVectorBased_BackAspect_ReturnsCorrectQuadrantValue(double obsX, double obsY, double obsZ, int expectedQuadrantColumn) + { + var rcsPattern = CreateAngleTestPattern(); + var observerDirection = new Vector3D(obsX, obsY, obsZ); // Observer is mostly along negative X + int expectedMajorAspectRow = 1; // Back + double expectedValue = (expectedMajorAspectRow + 1) * 100 + (expectedQuadrantColumn + 1) * 10; + double actualValue = rcsPattern.GetRcsValueDbSm(observerDirection, TARGET_FORWARD, TARGET_UP, TARGET_RIGHT); + Assert.AreEqual(expectedValue, actualValue, TEST_DELTA, $"Vector Back Aspect - Failed for Obs: {observerDirection}."); + } + + // Right Aspect (Row 3) - Observer to the right (positive Z relative to target right if TARGET_RIGHT is (0,0,1)) + [DataTestMethod] + // For Right view (majorAspectRow 3), localRight is -targetForward, localUp is targetUp. + // Q0 (LocalRight, LocalUp) -> dot(obs, -targetForward) >= 0, dot(obs, targetUp) >= 0 => dot(obs, targetForward) <=0, dot(obs, targetUp) >=0 + // If targetForward is X-axis, targetUp is Y-axis: + // Q0: X<=0, Y>=0. Example: obs=(-1, 1, 10) + // Q1: X>0, Y>=0. Example: obs=(1, 1, 10) + // Q2: X>0, Y<0. Example: obs=(1, -1, 10) + // Q3: X<=0, Y<0. Example: obs=(-1, -1, 10) + [DataRow(-1.0, 1.0, 10.0, 0, DisplayName = "VectorRight_UpFrontOfTarget_Q0")] + [DataRow(1.0, 1.0, 10.0, 1, DisplayName = "VectorRight_UpBackOfTarget_Q1")] + [DataRow(1.0, -1.0, 10.0, 2, DisplayName = "VectorRight_DownBackOfTarget_Q2")] + [DataRow(-1.0, -1.0, 10.0, 3, DisplayName = "VectorRight_DownFrontOfTarget_Q3")] + public void GetRcsVectorBased_RightAspect_ReturnsCorrectQuadrantValue(double obsX, double obsY, double obsZ, int expectedQuadrantColumn) + { + var rcsPattern = CreateAngleTestPattern(); + var observerDirection = new Vector3D(obsX, obsY, obsZ); // Observer is mostly along positive Z + int expectedMajorAspectRow = 3; // Right + double expectedValue = (expectedMajorAspectRow + 1) * 100 + (expectedQuadrantColumn + 1) * 10; + double actualValue = rcsPattern.GetRcsValueDbSm(observerDirection, TARGET_FORWARD, TARGET_UP, TARGET_RIGHT); + Assert.AreEqual(expectedValue, actualValue, TEST_DELTA, $"Vector Right Aspect - Failed for Obs: {observerDirection}."); + } + + // Left Aspect (Row 2) - Observer to the left (negative Z relative to target right) + [DataTestMethod] + // For Left view (majorAspectRow 2), localRight is targetForward, localUp is targetUp. + // Q0 (LocalRight, LocalUp) -> dot(obs, targetForward) >= 0, dot(obs, targetUp) >= 0 + // If targetForward is X-axis, targetUp is Y-axis: + // Q0: X>=0, Y>=0. Example: obs=(1, 1, -10) + // Q1: X<0, Y>=0. Example: obs=(-1, 1, -10) + // Q2: X<0, Y<0. Example: obs=(-1, -1, -10) + // Q3: X>=0, Y<0. Example: obs=(1, -1, -10) + [DataRow(1.0, 1.0, -10.0, 0, DisplayName = "VectorLeft_UpBackOfTarget_Q0")] + [DataRow(-1.0, 1.0, -10.0, 1, DisplayName = "VectorLeft_UpFrontOfTarget_Q1")] + [DataRow(-1.0, -1.0, -10.0, 2, DisplayName = "VectorLeft_DownFrontOfTarget_Q2")] + [DataRow(1.0, -1.0, -10.0, 3, DisplayName = "VectorLeft_DownBackOfTarget_Q3")] + public void GetRcsVectorBased_LeftAspect_ReturnsCorrectQuadrantValue(double obsX, double obsY, double obsZ, int expectedQuadrantColumn) + { + var rcsPattern = CreateAngleTestPattern(); + var observerDirection = new Vector3D(obsX, obsY, obsZ); // Observer is mostly along negative Z + int expectedMajorAspectRow = 2; // Left + double expectedValue = (expectedMajorAspectRow + 1) * 100 + (expectedQuadrantColumn + 1) * 10; + double actualValue = rcsPattern.GetRcsValueDbSm(observerDirection, TARGET_FORWARD, TARGET_UP, TARGET_RIGHT); + Assert.AreEqual(expectedValue, actualValue, TEST_DELTA, $"Vector Left Aspect - Failed for Obs: {observerDirection}."); + } + + [TestMethod] + public void GetRcsVectorBased_SpecialVectors_ReturnsExpected() + { + var rcsPattern = CreateAngleTestPattern(); + + // Observer directly along an axis (should pick a consistent quadrant) + // Directly Forward (X-axis): Should be Front (Row 0) + // Dot with Up (Y) is 0, Dot with Right (Z) is 0. isLocallyTop=true, isLocallyRight=true. -> Q0 + double expectedFrontQ0 = (0 + 1) * 100 + (0 + 1) * 10; // Row 0, Col 0 + Assert.AreEqual(expectedFrontQ0, rcsPattern.GetRcsValueDbSm(new Vector3D(1,0,0), TARGET_FORWARD, TARGET_UP, TARGET_RIGHT), TEST_DELTA, "Directly Forward vector."); + + // Directly Up (Y-axis): Should be Top (Row 4) + // Dot with targetForward (X) is 0. isLocallyTopQuadrant for Top/Bottom is dot(obs, forward) >= 0 -> true. + // Dot with targetRight (Z) is 0. isLocallyRightQuadrant for Top/Bottom is dot(obs, right) >= 0 -> true. + // Row 4, Col 0 (LocalRight, LocalUp for Top view means Z>=0, -X>=0. Obs=(0,1,0) -> Z=0, X=0. -> Q0) + // Correction: For Top view (row 4), localUp is -forwardNorm. observerDirNorm is (0,1,0). localUp is (-1,0,0). + // isLocallyTopQuadrant = DotProduct((0,1,0), (-1,0,0)) = 0 >=0 -> true. + // localRight is rightNorm (0,0,1). isLocallyRightQuadrant = DotProduct((0,1,0), (0,0,1)) = 0 >=0 -> true. + // So Q0 for Top is correct. + double expectedTopQ0 = (4 + 1) * 100 + (0 + 1) * 10; // Row 4, Col 0 + Assert.AreEqual(expectedTopQ0, rcsPattern.GetRcsValueDbSm(new Vector3D(0,1,0), TARGET_FORWARD, TARGET_UP, TARGET_RIGHT), TEST_DELTA, "Directly Up vector."); + + // Observer direction is Zero vector (should normalize to something, or be handled gracefully) + // Vector3D.Normalize() of Zero vector returns Zero vector. Math.Asin(NaN) if DotProduct is weird. + // Current RcsPattern logic: Normalize of (0,0,0) is (0,0,0). + // Dot products will be 0. Asin(0)=0. GlobalEl=0, GlobalAz=0. -> Front (Row 0). + // localRight=Z, localUp=Y. isLocallyTop=true, isLocallyRight=true -> Q0. + // So, (0,0,0) observer direction should result in Front, Q0 + Assert.AreEqual(expectedFrontQ0, rcsPattern.GetRcsValueDbSm(Vector3D.Zero, TARGET_FORWARD, TARGET_UP, TARGET_RIGHT), TEST_DELTA, "Zero observer vector."); + + // Test case where projectionOnHorizontalPlane is zero (observer directly above/below, not on main axis for az calc) + // Observer directly along Up vector (0,1,0) has already been tested and gives Top Q0. + // Observer directly along Down vector (0,-1,0) -> Bottom (Row 5) + // For Bottom (row 5), localUp is forwardNorm (X). isLocallyTopQuadrant = dot((0,-1,0), (1,0,0)) = 0 >=0 -> true. + // localRight is rightNorm (Z). isLocallyRightQuadrant = dot((0,-1,0), (0,0,1)) = 0 >=0 -> true. + // So Q0 for Bottom view. + double expectedBottomQ0 = (5+1)*100 + (0+1)*10; // Row 5, Col 0 + Assert.AreEqual(expectedBottomQ0, rcsPattern.GetRcsValueDbSm(new Vector3D(0,-1,0), TARGET_FORWARD, TARGET_UP, TARGET_RIGHT), TEST_DELTA, "Directly Down vector."); + } + + [TestMethod] + public void GetAverageRcsDbSm_ReturnsCorrectAverage() + { + // Arrange + // Create a matrix where average is easy to calculate: e.g., all values are 10.0 + var testMatrix = CreateTestMatrix(6, 4, (r, c) => 10.0); + var rcsPattern = new RcsPattern(testMatrix); + double expectedAverage = 10.0; + + // Act + double actualAverage = rcsPattern.GetAverageRcsDbSm(); + + // Assert + Assert.AreEqual(expectedAverage, actualAverage, TEST_DELTA, "Average calculation is incorrect."); + + // Arrange: Test with varied values + var variedMatrix = CreateTestMatrix(); // Uses (r*10+c+1) + // Sum = (1+2+3+4) + (11+12+13+14) + ... + (51+52+53+54) + // Row sums: 10, 50, 90, 130, 170, 210. Total sum = 660. + // Count = 6 * 4 = 24. + // Expected average = 660 / 24 = 27.5 + rcsPattern = new RcsPattern(variedMatrix); + expectedAverage = 660.0 / 24.0; + + // Act + actualAverage = rcsPattern.GetAverageRcsDbSm(); + + // Assert + Assert.AreEqual(expectedAverage, actualAverage, TEST_DELTA, "Average calculation for varied matrix is incorrect."); + } + + [TestMethod] + public void GetAverageRcsDbSm_WhenDataSourceIsNull_ReturnsDefault() + { + // Arrange + var rcsPattern = new RcsPattern + { + RcsMatrixDataSource = null! // Force invalid state + }; + + // Act + double actualAverage = rcsPattern.GetAverageRcsDbSm(); + + // Assert + // When RcsMatrixDataSource is null, RcsMatrixData (getter) will return a default-filled grid. + // So, the average should be DEFAULT_RCS_DBSM_IN_PATTERN. + Assert.AreEqual(DEFAULT_RCS_DBSM_IN_PATTERN, actualAverage, TEST_DELTA, "Average should be default when DataSource is null."); + } + } +} \ No newline at end of file diff --git a/ThreatSource.Tests/src/Target/TankTests.cs b/ThreatSource.Tests/src/Equipment/TankTests.cs similarity index 97% rename from ThreatSource.Tests/src/Target/TankTests.cs rename to ThreatSource.Tests/src/Equipment/TankTests.cs index 2327b2e..c5ab078 100644 --- a/ThreatSource.Tests/src/Target/TankTests.cs +++ b/ThreatSource.Tests/src/Equipment/TankTests.cs @@ -109,7 +109,7 @@ namespace ThreatSource.Tests.Target public void GetRadarCrossSection_ReturnsExpectedValue() { // Arrange - var observerPosition = new Vector3D(0, 0, 0); + Vector3D observerPosition = new(0, 0, 0); // Act double rcs = _tank.Properties.RadarCrossSection; @@ -122,7 +122,7 @@ namespace ThreatSource.Tests.Target public void GetInfraredSignature_ReturnsExpectedValue() { // Arrange - var observerPosition = new Vector3D(0, 0, 0); + Vector3D observerPosition = new(0, 0, 0); // Act double signature = _tank.Properties.InfraredRadiationIntensity; diff --git a/ThreatSource.Tests/src/Jamming/InfraredImagingGuidanceJammingTests.cs b/ThreatSource.Tests/src/Jamming/InfraredImagingGuidanceJammingTests.cs index 345da5e..4525def 100644 --- a/ThreatSource.Tests/src/Jamming/InfraredImagingGuidanceJammingTests.cs +++ b/ThreatSource.Tests/src/Jamming/InfraredImagingGuidanceJammingTests.cs @@ -67,10 +67,10 @@ namespace ThreatSource.Tests.Jamming var missileMotion = new KinematicState { Position = initialPosition, - Speed = initialVelocity.Magnitude() + Speed = initialVelocity.Magnitude(), + Orientation = new Orientation(0, 0, 0) }; - missileMotion.Orientation = new Orientation(0, 0, 0); - + var missileProperties = new MissileProperties { MaxSpeed = 1000, diff --git a/ThreatSource.Tests/src/Jamming/LaserBeamRiderGuidanceJammingTests.cs b/ThreatSource.Tests/src/Jamming/LaserBeamRiderGuidanceJammingTests.cs index 08c99c1..4dd1799 100644 --- a/ThreatSource.Tests/src/Jamming/LaserBeamRiderGuidanceJammingTests.cs +++ b/ThreatSource.Tests/src/Jamming/LaserBeamRiderGuidanceJammingTests.cs @@ -74,15 +74,15 @@ namespace ThreatSource.Tests.Jamming _testAdapter.AddTestEntity("target1", _target); // 创建导弹对象 - var initialPosition = new Vector3D(500, 0, 0); - var initialVelocity = new Vector3D(100, 0, 0); - var missileMotion = new KinematicState + Vector3D initialPosition = new(500, 0, 0); + Vector3D initialVelocity = new(100, 0, 0); + KinematicState missileMotion = new() { Position = initialPosition, - Speed = initialVelocity.Magnitude() + Speed = initialVelocity.Magnitude(), + Orientation = new Orientation(0, 0, 0) }; - missileMotion.Orientation = new Orientation(0, 0, 0); - + var missileProperties = new MissileProperties { MaxSpeed = 1000, diff --git a/ThreatSource.Tests/src/Jamming/LaserDecoyTests.cs b/ThreatSource.Tests/src/Jamming/LaserDecoyTests.cs index c045e63..31b0799 100644 --- a/ThreatSource.Tests/src/Jamming/LaserDecoyTests.cs +++ b/ThreatSource.Tests/src/Jamming/LaserDecoyTests.cs @@ -725,7 +725,7 @@ namespace ThreatSource.Tests.Jamming } // 通过反射获取TargetPosition属性 - var targetPositionProperty = typeof(LaserSemiActiveGuidanceSystem).GetProperty("TargetPosition", + var targetPositionProperty = typeof(LaserSemiActiveGuidanceSystem).GetField("TargetPosition", System.Reflection.BindingFlags.NonPublic | System.Reflection.BindingFlags.Instance); // 多次更新仿真系统和各组件,确保激光照射事件被处理 @@ -738,7 +738,8 @@ namespace ThreatSource.Tests.Jamming } // 记录初始状态下的目标位置 - Vector3D initialTargetPosition = (targetPositionProperty?.GetValue(_guidanceSystem) as Vector3D) ?? Vector3D.Zero; + Vector3D? nullableInitialTargetPosition = targetPositionProperty?.GetValue(_guidanceSystem) as Vector3D?; + Vector3D initialTargetPosition = nullableInitialTargetPosition ?? Vector3D.Zero; Debug.WriteLine($"初始识别的目标位置: {initialTargetPosition}"); // 发射激光诱偏 - 在真实目标的不同方向 @@ -765,7 +766,8 @@ namespace ThreatSource.Tests.Jamming } // 获取诱偏目标位置 - Vector3D decoyPosition = (targetPositionProperty?.GetValue(_guidanceSystem) as Vector3D) ?? Vector3D.Zero; + Vector3D? nullableDecoyPosition = targetPositionProperty?.GetValue(_guidanceSystem) as Vector3D?; + Vector3D decoyPosition = nullableDecoyPosition ?? Vector3D.Zero; Debug.WriteLine($"诱偏目标位置: {decoyPosition}"); // 计算诱偏目标到诱偏源的实际距离 @@ -779,7 +781,8 @@ namespace ThreatSource.Tests.Jamming Debug.WriteLine($"导弹到真实目标距离: {missileToDReal}米, 导弹到诱偏目标距离: {missileToDDecoy}米"); // 获取诱偏后制导系统识别的目标位置 - Vector3D compositePosition = (targetPositionProperty?.GetValue(_guidanceSystem) as Vector3D) ?? Vector3D.Zero; + Vector3D? nullableCompositePosition = targetPositionProperty?.GetValue(_guidanceSystem) as Vector3D?; + Vector3D compositePosition = nullableCompositePosition ?? Vector3D.Zero; Debug.WriteLine($"合成后的目标位置: {compositePosition}"); // 获取导弹当前的视场角 @@ -973,7 +976,7 @@ namespace ThreatSource.Tests.Jamming }; // 通过反射获取TargetPosition属性 - var targetPositionProperty = typeof(LaserSemiActiveGuidanceSystem).GetProperty("TargetPosition", + var targetPositionProperty = typeof(LaserSemiActiveGuidanceSystem).GetField("TargetPosition", System.Reflection.BindingFlags.NonPublic | System.Reflection.BindingFlags.Instance); // 获取计算接收功率的方法 @@ -1011,7 +1014,8 @@ namespace ThreatSource.Tests.Jamming } // 记录初始状态下的目标位置 - Vector3D initialTargetPosition = targetPositionProperty?.GetValue(_guidanceSystem) as Vector3D ?? Vector3D.Zero; + Vector3D? nullableInitialTargetPosition = targetPositionProperty?.GetValue(_guidanceSystem) as Vector3D?; + Vector3D initialTargetPosition = nullableInitialTargetPosition ?? Vector3D.Zero; Debug.WriteLine($"初始识别的目标位置: {initialTargetPosition}"); // 获取真实目标的接收功率 @@ -1080,7 +1084,8 @@ namespace ThreatSource.Tests.Jamming } // 获取诱偏目标位置 - Vector3D decoyPosition = targetPositionProperty?.GetValue(_guidanceSystem) as Vector3D ?? Vector3D.Zero; + Vector3D? nullableDecoyPosition = targetPositionProperty?.GetValue(_guidanceSystem) as Vector3D?; + Vector3D decoyPosition = nullableDecoyPosition ?? Vector3D.Zero; Debug.WriteLine($"诱偏目标位置: {decoyPosition}"); // 计算诱偏目标到诱偏源的实际距离 diff --git a/ThreatSource.Tests/src/Jamming/LaserSemiActiveGuidanceJammingTests.cs b/ThreatSource.Tests/src/Jamming/LaserSemiActiveGuidanceJammingTests.cs index c5c1599..1f40624 100644 --- a/ThreatSource.Tests/src/Jamming/LaserSemiActiveGuidanceJammingTests.cs +++ b/ThreatSource.Tests/src/Jamming/LaserSemiActiveGuidanceJammingTests.cs @@ -131,8 +131,8 @@ namespace ThreatSource.Tests.Jamming Assert.IsNotNull(_missile); // Arrange - var initialPosition = new Vector3D(500, 0, 0); - var initialVelocity = new Vector3D(100, 0, 0); + Vector3D initialPosition = new(500, 0, 0); + Vector3D initialVelocity = new(100, 0, 0); System.Diagnostics.Debug.WriteLine($"测试开始 - 初始位置: {initialPosition}, 初始速度: {initialVelocity}"); @@ -223,8 +223,8 @@ namespace ThreatSource.Tests.Jamming Assert.IsNotNull(_missile); // Arrange - var initialPosition = new Vector3D(500, 0, 0); - var initialVelocity = new Vector3D(100, 0, 0); + Vector3D initialPosition = new(500, 0, 0); + Vector3D initialVelocity = new(100, 0, 0); // 更新导弹位置和速度 _missile.KState.Position = initialPosition; @@ -266,8 +266,8 @@ namespace ThreatSource.Tests.Jamming Assert.IsNotNull(_missile); // Arrange - var initialPosition = new Vector3D(500, 0, 0); - var initialVelocity = new Vector3D(100, 0, 0); + Vector3D initialPosition = new(500, 0, 0); + Vector3D initialVelocity = new(100, 0, 0); // 更新导弹位置和速度 _missile.KState.Position = initialPosition; @@ -319,8 +319,8 @@ namespace ThreatSource.Tests.Jamming Assert.IsNotNull(_missile); // Arrange - var initialPosition = new Vector3D(500, 0, 0); - var initialVelocity = new Vector3D(100, 0, 0); + Vector3D initialPosition = new(500, 0, 0); + Vector3D initialVelocity = new(100, 0, 0); // 更新导弹位置和速度 _missile.KState.Position = initialPosition; @@ -369,8 +369,8 @@ namespace ThreatSource.Tests.Jamming Assert.IsNotNull(_missile); // Arrange - var initialPosition = new Vector3D(500, 0, 0); - var initialVelocity = new Vector3D(100, 0, 0); + Vector3D initialPosition = new(500, 0, 0); + Vector3D initialVelocity = new(100, 0, 0); // 更新导弹位置和速度 _missile.KState.Position = initialPosition; diff --git a/ThreatSource.Tests/src/Jamming/MillimeterWaveGuidanceJammingTests.cs b/ThreatSource.Tests/src/Jamming/MillimeterWaveGuidanceJammingTests.cs index e953d47..0d94165 100644 --- a/ThreatSource.Tests/src/Jamming/MillimeterWaveGuidanceJammingTests.cs +++ b/ThreatSource.Tests/src/Jamming/MillimeterWaveGuidanceJammingTests.cs @@ -127,8 +127,8 @@ namespace ThreatSource.Tests.Jamming Assert.IsNotNull(_missile); // Arrange - var initialPosition = new Vector3D(500, 0, 0); - var initialVelocity = new Vector3D(100, 0, 0); + Vector3D initialPosition = new(500, 0, 0); + Vector3D initialVelocity = new(100, 0, 0); System.Diagnostics.Debug.WriteLine($"测试开始 - 初始位置: {initialPosition}, 初始速度: {initialVelocity}"); @@ -176,8 +176,8 @@ namespace ThreatSource.Tests.Jamming Assert.IsNotNull(_missile); // Arrange - var initialPosition = new Vector3D(500, 0, 0); - var initialVelocity = new Vector3D(100, 0, 0); + Vector3D initialPosition = new(500, 0, 0); + Vector3D initialVelocity = new(100, 0, 0); // 更新导弹位置和速度 _missile.KState.Position = initialPosition; @@ -218,8 +218,8 @@ namespace ThreatSource.Tests.Jamming Assert.IsNotNull(_missile); // Arrange - var initialPosition = new Vector3D(500, 0, 0); - var initialVelocity = new Vector3D(100, 0, 0); + Vector3D initialPosition = new(500, 0, 0); + Vector3D initialVelocity = new(100, 0, 0); // 更新导弹位置和速度 _missile.KState.Position = initialPosition; @@ -258,8 +258,8 @@ namespace ThreatSource.Tests.Jamming Assert.IsNotNull(_missile); // Arrange - var initialPosition = new Vector3D(500, 0, 0); - var initialVelocity = new Vector3D(100, 0, 0); + Vector3D initialPosition = new(500, 0, 0); + Vector3D initialVelocity = new(100, 0, 0); // 更新导弹位置和速度 _missile.KState.Position = initialPosition; @@ -311,8 +311,8 @@ namespace ThreatSource.Tests.Jamming Assert.IsNotNull(_missile); // Arrange - var initialPosition = new Vector3D(500, 0, 0); - var initialVelocity = new Vector3D(100, 0, 0); + Vector3D initialPosition = new(500, 0, 0); + Vector3D initialVelocity = new(100, 0, 0); // 更新导弹位置和速度 _missile.KState.Position = initialPosition; @@ -361,8 +361,8 @@ namespace ThreatSource.Tests.Jamming Assert.IsNotNull(_missile); // Arrange - var initialPosition = new Vector3D(500, 0, 0); - var initialVelocity = new Vector3D(100, 0, 0); + Vector3D initialPosition = new(500, 0, 0); + Vector3D initialVelocity = new(100, 0, 0); // 更新导弹位置和速度 _missile.KState.Position = initialPosition; diff --git a/ThreatSource.Tests/src/Jamming/SmokeGrenadeTests.cs b/ThreatSource.Tests/src/Jamming/SmokeGrenadeTests.cs index 2929143..b904350 100644 --- a/ThreatSource.Tests/src/Jamming/SmokeGrenadeTests.cs +++ b/ThreatSource.Tests/src/Jamming/SmokeGrenadeTests.cs @@ -188,9 +188,9 @@ namespace ThreatSource.Tests.Jamming Duration = 60.0, CloudDiameter = 20.0 }; - var initialPosition = new Vector3D(0, 0, 0); - var motionParams = new KinematicState { Position = initialPosition }; - var smokeGrenade = new SmokeGrenade("test", config, motionParams, _simulationManager); + Vector3D initialPosition = new(0, 0, 0); + KinematicState motionParams = new() { Position = initialPosition }; + SmokeGrenade smokeGrenade = new("test", config, motionParams, _simulationManager); smokeGrenade.Activate(); // Act @@ -299,11 +299,11 @@ namespace ThreatSource.Tests.Jamming public void CloudSmoke_HighObserverToLowTarget_ThroughSmoke_CalculatesCorrectThickness() { // Arrange - var observerPosition = new Vector3D(5, 50, 0); - var targetPosition = new Vector3D(0, 0, 0); - var smokeCenterPosition = new Vector3D(0, 20, 0); + Vector3D observerPosition = new(5, 50, 0); + Vector3D targetPosition = new(0, 0, 0); + Vector3D smokeCenterPosition = new(0, 20, 0); - var config = new SmokeGrenadeConfig + SmokeGrenadeConfig config = new() { SmokeType = SmokeScreenType.Cloud, CloudDiameter = 20.0, @@ -312,8 +312,8 @@ namespace ThreatSource.Tests.Jamming Duration = 60.0 }; - var motionParams = new KinematicState { Position = smokeCenterPosition }; - var smokeGrenade = new SmokeGrenade("smoke_high_obs_test", config, motionParams, _simulationManager); + KinematicState motionParams = new() { Position = smokeCenterPosition }; + SmokeGrenade smokeGrenade = new("smoke_high_obs_test", config, motionParams, _simulationManager); smokeGrenade.Activate(); diff --git a/ThreatSource.Tests/src/Missile/TerminalSensitiveSubmunitionTests.cs b/ThreatSource.Tests/src/Missile/TerminalSensitiveSubmunitionTests.cs index 05162c9..3d8bc3a 100644 --- a/ThreatSource.Tests/src/Missile/TerminalSensitiveSubmunitionTests.cs +++ b/ThreatSource.Tests/src/Missile/TerminalSensitiveSubmunitionTests.cs @@ -188,14 +188,14 @@ namespace ThreatSource.Tests.Missile double spiralAngle = 90 * Math.PI / 180; // 转换为弧度 // 计算扫描方向(考虑锥角) - Vector3D scanDirection = new Vector3D( + Vector3D scanDirection = new( Math.Cos(spiralAngle) * Math.Sin(30 * Math.PI / 180), -Math.Cos(30 * Math.PI / 180), Math.Sin(spiralAngle) * Math.Sin(30 * Math.PI / 180) ); // 计算水平面上的扫描方向 - Vector3D horizontalScanDirection = new Vector3D( + Vector3D horizontalScanDirection = new( Math.Cos(spiralAngle), 0, Math.Sin(spiralAngle) @@ -262,18 +262,18 @@ namespace ThreatSource.Tests.Missile try { // 设置目标位置 - var targetPosition = new Vector3D(targetX, targetY, targetZ); + Vector3D targetPosition = new(targetX, targetY, targetZ); _tank.KState.Position = targetPosition; // 设置子弹位置 - var submunitionPosition = new Vector3D(submunitionX, submunitionY, submunitionZ); + Vector3D submunitionPosition = new(submunitionX, submunitionY, submunitionZ); _submunition.KState.Position = submunitionPosition; // 计算从子弹到目标的向量 - var toTarget = (targetPosition - submunitionPosition).Normalize(); + Vector3D toTarget = (targetPosition - submunitionPosition).Normalize(); // 设置子弹的扫描方向直接指向目标 - var scanDirection = toTarget; + Vector3D scanDirection = toTarget; var field = typeof(TerminalSensitiveSubmunition) .GetField("scanDirection", System.Reflection.BindingFlags.NonPublic | System.Reflection.BindingFlags.Instance); diff --git a/ThreatSource.Tests/src/Sensor/QuadrantDetectorTests.cs b/ThreatSource.Tests/src/Sensor/QuadrantDetectorTests.cs index 0d848b0..26d8536 100644 --- a/ThreatSource.Tests/src/Sensor/QuadrantDetectorTests.cs +++ b/ThreatSource.Tests/src/Sensor/QuadrantDetectorTests.cs @@ -50,7 +50,7 @@ namespace ThreatSource.Tests.Sensor { // Arrange double receivedPower = _minDetectablePower * 0.5; // 低于阈值 - Vector2D spotOffset = new Vector2D(0, 0); // 中心位置 + Vector2D spotOffset = new(0, 0); // 中心位置 // Act _detector.ProcessLaserSignal(receivedPower, spotOffset); @@ -67,7 +67,7 @@ namespace ThreatSource.Tests.Sensor { // Arrange double receivedPower = _minDetectablePower * 2.0; // 高于阈值 - Vector2D spotOffset = new Vector2D(0, 0); // 中心位置 + Vector2D spotOffset = new(0, 0); // 中心位置 // Act _detector.ProcessLaserSignal(receivedPower, spotOffset); @@ -89,7 +89,7 @@ namespace ThreatSource.Tests.Sensor { // Arrange double receivedPower = _minDetectablePower * 10.0; // 充分高于阈值 - Vector2D spotOffset = new Vector2D(offsetX, offsetY); + Vector2D spotOffset = new(offsetX, offsetY); // Act _detector.ProcessLaserSignal(receivedPower, spotOffset); @@ -105,7 +105,7 @@ namespace ThreatSource.Tests.Sensor public void GetTargetDirection_NotLocked_ReturnsCurrentDirection() { // Arrange - Vector3D currentDirection = new Vector3D(1, 0, 0); + Vector3D currentDirection = new(1, 0, 0); double sensitivity = 0.5; // Act - 不调用ProcessLaserSignal,保持未锁定状态 @@ -119,10 +119,10 @@ namespace ThreatSource.Tests.Sensor public void GetTargetDirection_Locked_CenterSpot_ReturnsCurrentDirection() { // Arrange - Vector3D currentDirection = new Vector3D(1, 0, 0); + Vector3D currentDirection = new(1, 0, 0); double sensitivity = 0.5; double receivedPower = _minDetectablePower * 10.0; - Vector2D spotOffset = new Vector2D(0, 0); // 中心位置 + Vector2D spotOffset = new(0, 0); // 中心位置 // Act _detector.ProcessLaserSignal(receivedPower, spotOffset); @@ -139,10 +139,10 @@ namespace ThreatSource.Tests.Sensor public void GetTargetDirection_Locked_RightOffset_CorrectlyAdjusts() { // Arrange - Vector3D currentDirection = new Vector3D(1, 0, 0); + Vector3D currentDirection = new(1, 0, 0); double sensitivity = 0.5; double receivedPower = _minDetectablePower * 10.0; - Vector2D spotOffset = new Vector2D(0.05, 0); // 右偏,使用更大的偏移 + Vector2D spotOffset = new(0.05, 0); // 右偏,使用更大的偏移 // Act _detector.ProcessLaserSignal(receivedPower, spotOffset); @@ -162,10 +162,10 @@ namespace ThreatSource.Tests.Sensor public void GetTargetDirection_Locked_UpOffset_CorrectlyAdjusts() { // Arrange - Vector3D currentDirection = new Vector3D(1, 0, 0); + Vector3D currentDirection = new(1, 0, 0); double sensitivity = 0.5; double receivedPower = _minDetectablePower * 10.0; - Vector2D spotOffset = new Vector2D(0, 0.05); // 上偏,使用更大的偏移 + Vector2D spotOffset = new(0, 0.05); // 上偏,使用更大的偏移 // Act _detector.ProcessLaserSignal(receivedPower, spotOffset); @@ -186,7 +186,7 @@ namespace ThreatSource.Tests.Sensor { // Arrange double receivedPower = _minDetectablePower * 5.0; - Vector2D spotOffset = new Vector2D(0.005, -0.005); + Vector2D spotOffset = new(0.005, -0.005); _detector.ProcessLaserSignal(receivedPower, spotOffset); // Act @@ -205,7 +205,7 @@ namespace ThreatSource.Tests.Sensor { // Arrange double receivedPower = _minDetectablePower * 10.0; - Vector2D spotOffset = new Vector2D(0.05, 0.05); // 极端偏移,超出正常范围 + Vector2D spotOffset = new(0.05, 0.05); // 极端偏移,超出正常范围 // Act _detector.ProcessLaserSignal(receivedPower, spotOffset); @@ -220,9 +220,9 @@ namespace ThreatSource.Tests.Sensor public void GetTargetDirection_DifferentSensitivity_ProportionalAdjustment() { // Arrange - Vector3D currentDirection = new Vector3D(1, 0, 0); + Vector3D currentDirection = new(1, 0, 0); double receivedPower = _minDetectablePower * 10.0; - Vector2D spotOffset = new Vector2D(0.01, 0); // 使用更小的偏移,符合实际情况 + Vector2D spotOffset = new(0.01, 0); // 使用更小的偏移,符合实际情况 // 使用更合理的灵敏度值 double standardSensitivity = 0.05; // 实际经验值 @@ -258,9 +258,9 @@ namespace ThreatSource.Tests.Sensor string testCondition) { // Arrange - Vector3D currentDirection = new Vector3D(1, 0, 0); + Vector3D currentDirection = new(1, 0, 0); double receivedPower = _minDetectablePower * 10.0; - Vector2D offset = new Vector2D(spotOffset, 0); // 水平偏移 + Vector2D offset = new(spotOffset, 0); // 水平偏移 // Act _detector.ProcessLaserSignal(receivedPower, offset); diff --git a/ThreatSource.Tests/src/Utils/MotionAlgorithmTests.cs b/ThreatSource.Tests/src/Utils/MotionAlgorithmTests.cs index 8fd6b2d..bd84f0d 100644 --- a/ThreatSource.Tests/src/Utils/MotionAlgorithmTests.cs +++ b/ThreatSource.Tests/src/Utils/MotionAlgorithmTests.cs @@ -15,9 +15,9 @@ namespace ThreatSource.Tests.Utils // 辅助函数,用于断言发射速度接近初始速度 private static void AssertLaunchSpeed(Vector3D? launchVelocity, double initialSpeed) { - if (launchVelocity == null) return; // 如果该值为空且不符合预期,则测试已失败 - Assert.IsTrue(Math.Abs(launchVelocity.Magnitude() - initialSpeed) < SpeedDelta, - $"Expected launch speed close to {initialSpeed}, but got {launchVelocity.Magnitude():F2}. Velocity: {launchVelocity}"); + if (!launchVelocity.HasValue) return; // 如果该值为空且不符合预期,则测试已失败 + Assert.IsTrue(Math.Abs(launchVelocity.Value.Magnitude() - initialSpeed) < SpeedDelta, + $"Expected launch speed close to {initialSpeed}, but got {launchVelocity.Value.Magnitude():F2}. Velocity: {launchVelocity.Value}"); } [TestMethod] @@ -37,8 +37,8 @@ namespace ThreatSource.Tests.Utils Assert.IsNotNull(launchVelocity, "Launch velocity should not be null for a reachable target."); AssertLaunchSpeed(launchVelocity, initialSpeed); // 我们期望 Y 分量略微为正,以抵消重力和初始阻力的 Y 分量 - Assert.IsTrue(launchVelocity.Y > -Epsilon, $"Expected Y component to be positive or near zero, but got {launchVelocity.Y:F2}"); - Assert.IsTrue(Math.Abs(launchVelocity.Z) < Epsilon + 5, $"Expected Z component to be near zero, but got {launchVelocity.Z:F2}"); // 由于阻力矢量影响,允许稍大的容差 + Assert.IsTrue(launchVelocity.Value.Y > -Epsilon, $"Expected Y component to be positive or near zero, but got {launchVelocity.Value.Y:F2}"); + Assert.IsTrue(Math.Abs(launchVelocity.Value.Z) < Epsilon + 5, $"Expected Z component to be near zero, but got {launchVelocity.Value.Z:F2}"); // 由于阻力矢量影响,允许稍大的容差 } [TestMethod] @@ -95,7 +95,7 @@ namespace ThreatSource.Tests.Utils // 断言 Assert.IsNotNull(launchVelocity, "Launch velocity should not be null."); AssertLaunchSpeed(launchVelocity, initialSpeed); - Assert.IsTrue(launchVelocity.Z < -Epsilon, $"Expected Z component to be negative to counteract side wind, got {launchVelocity.Z:F2}"); + Assert.IsTrue(launchVelocity.Value.Z < -Epsilon, $"Expected Z component to be negative to counteract side wind, got {launchVelocity.Value.Z:F2}"); } @@ -103,8 +103,8 @@ namespace ThreatSource.Tests.Utils public void CalculateLaunchVelocity_TargetUnreachable_SpeedTooLow_ShouldReturnNull() { // 准备 - Vector3D startPos = new Vector3D(0, 0, 0); - Vector3D targetPos = new Vector3D(10000, 0, 0); // 远距离目标 + Vector3D startPos = new(0, 0, 0); + Vector3D targetPos = new(10000, 0, 0); // 远距离目标 double initialSpeed = 100; // 低速 Vector3D windVector = Vector3D.Zero; @@ -120,8 +120,8 @@ namespace ThreatSource.Tests.Utils public void CalculateLaunchVelocity_StartAndTargetCoincide_InitialSpeedNonZero_ShouldReturnReasonable() { // 准备 - Vector3D startPos = new Vector3D(10, 20, 30); - Vector3D targetPos = new Vector3D(10, 20, 30); + Vector3D startPos = new(10, 20, 30); + Vector3D targetPos = new(10, 20, 30); double initialSpeed = 50; Vector3D windVector = Vector3D.Zero; @@ -144,15 +144,15 @@ namespace ThreatSource.Tests.Utils // 此处,initialSpeed 为 50,因此不采用特定的 Vector3D.Zero 路径。 // 它应该会遇到 t_final < 1e-6 的情况并返回 null。 - Assert.IsNull(launchVelocity, $"Expected null or specific vector when start=target, initialSpeed>0. Got: {launchVelocity}"); + Assert.IsNull(launchVelocity, $"Expected null or specific vector when start=target, initialSpeed>0. Got: {(launchVelocity.HasValue ? launchVelocity.Value.ToString() : "null")}"); } [TestMethod] public void CalculateLaunchVelocity_StartAndTargetCoincide_InitialSpeedZero_ShouldReturnZeroOrNull() { // 准备 - Vector3D startPos = new Vector3D(0, 0, 0); - Vector3D targetPos = new Vector3D(0, 0, 0); + Vector3D startPos = new(0, 0, 0); + Vector3D targetPos = new(0, 0, 0); double initialSpeed = 0; Vector3D windVector = Vector3D.Zero; @@ -166,8 +166,8 @@ namespace ThreatSource.Tests.Utils // Coeff_c (deltaPos.MagSq) 为 0。Coeff_b 为 -(0 + 0) = 0。Coeff_a 为 0.25 * Gravity.MagSq > 0。 // T_sol1 = (0 + 0) / (2a) = 0。T_sol2 = (0-0) / (2a) = 0。所以 t_final = 0。 // 然后,如果 deltaPos.MagSq < 1e-9 && initialSpeed < 1e-6,则返回 Vector3D.Zero。此条件已满足。 - Assert.IsNotNull(launchVelocity, "Launch velocity should not be null."); - Assert.AreEqual(0, launchVelocity.MagnitudeSquared(), Epsilon, "Expected Zero vector."); + Assert.IsTrue(launchVelocity.HasValue, "Launch velocity should not be null."); + Assert.AreEqual(0, launchVelocity.Value.MagnitudeSquared(), Epsilon, "Expected Zero vector."); } @@ -175,8 +175,8 @@ namespace ThreatSource.Tests.Utils public void CalculateLaunchVelocity_VerticalShot_NoWind_ShouldHit() { // 准备 - Vector3D startPos = new Vector3D(0, 0, 0); - Vector3D targetPos = new Vector3D(0, 100, 0); // 目标正上方 + Vector3D startPos = new(0, 0, 0); + Vector3D targetPos = new(0, 100, 0); // 目标正上方 double initialSpeed = 150; // 足够的速度 Vector3D windVector = Vector3D.Zero; @@ -187,19 +187,20 @@ namespace ThreatSource.Tests.Utils // 断言 Assert.IsNotNull(launchVelocity, "Launch velocity should not be null for a vertical shot."); AssertLaunchSpeed(launchVelocity, initialSpeed); - Assert.IsTrue(launchVelocity.Y > 0, $"Expected Y component to be positive, got {launchVelocity.Y:F2}"); - Assert.IsTrue(Math.Abs(launchVelocity.X) < Epsilon + 0.1, $"Expected X component to be near zero, got {launchVelocity.X:F2}"); // 由于阻力计算,允许一些小的值 - Assert.IsTrue(Math.Abs(launchVelocity.Z) < Epsilon + 0.1, $"Expected Z component to be near zero, got {launchVelocity.Z:F2}"); + Assert.IsTrue(launchVelocity.HasValue); // 确保 launchVelocity.Value 可以安全访问 + Assert.IsTrue(launchVelocity.Value.Y > 0, $"Expected Y component to be positive, got {launchVelocity.Value.Y:F2}"); + Assert.IsTrue(Math.Abs(launchVelocity.Value.X) < Epsilon + 0.1, $"Expected X component to be near zero, got {launchVelocity.Value.X:F2}"); // 由于阻力计算,允许一些小的值 + Assert.IsTrue(Math.Abs(launchVelocity.Value.Z) < Epsilon + 0.1, $"Expected Z component to be near zero, got {launchVelocity.Value.Z:F2}"); } [TestMethod] public void CalculateLaunchVelocity_WithInitialGuess_ShouldProduceValidResult() { // 准备 - Vector3D startPos = new Vector3D(0, 0, 0); - Vector3D targetPos = new Vector3D(500, 50, 0); + Vector3D startPos = new(0, 0, 0); + Vector3D targetPos = new(500, 50, 0); double initialSpeed = 200; - Vector3D windVector = new Vector3D(5,0,2); + Vector3D windVector = new(5,0,2); Vector3D initialGuess = new Vector3D(150, 100, 10).Normalize() * initialSpeed; // 一个合理的猜测 // 执行 diff --git a/ThreatSource.Tests/src/Utils/ObscurationUtilsTests.cs b/ThreatSource.Tests/src/Utils/ObscurationUtilsTests.cs index 3b42aa9..52dfce7 100644 --- a/ThreatSource.Tests/src/Utils/ObscurationUtilsTests.cs +++ b/ThreatSource.Tests/src/Utils/ObscurationUtilsTests.cs @@ -16,12 +16,12 @@ namespace ThreatSource.Tests.Utils public void CalculateProjectedOverlapRatio_ForegroundBehindBackground_ReturnsZero() { // Arrange - Vector3D observerPos = new Vector3D(0, 0, -10); - Vector3D backgroundCenter = new Vector3D(0, 0, 0); - Vector3D backgroundDims = new Vector3D(2, 2, 2); + Vector3D observerPos = new(0, 0, -10); + Vector3D backgroundCenter = new(0, 0, 0); + Vector3D backgroundDims = new(2, 2, 2); Orientation backgroundOrient = IdentityOrientation(); - Vector3D foregroundCenter = new Vector3D(0, 0, 5); // Foreground is behind background - Vector3D foregroundDims = new Vector3D(1, 1, 1); + Vector3D foregroundCenter = new(0, 0, 5); // Foreground is behind background + Vector3D foregroundDims = new(1, 1, 1); Orientation foregroundOrient = IdentityOrientation(); // Act @@ -37,12 +37,12 @@ namespace ThreatSource.Tests.Utils public void CalculateProjectedOverlapRatio_ForegroundToTheSide_ReturnsZero() { // Arrange - Vector3D observerPos = new Vector3D(0, 0, -10); - Vector3D backgroundCenter = new Vector3D(0, 0, 0); - Vector3D backgroundDims = new Vector3D(2, 2, 2); + Vector3D observerPos = new(0, 0, -10); + Vector3D backgroundCenter = new(0, 0, 0); + Vector3D backgroundDims = new(2, 2, 2); Orientation backgroundOrient = IdentityOrientation(); - Vector3D foregroundCenter = new Vector3D(5, 0, 0); // Foreground is to the side - Vector3D foregroundDims = new Vector3D(1, 1, 1); + Vector3D foregroundCenter = new(5, 0, 0); // Foreground is to the side + Vector3D foregroundDims = new(1, 1, 1); Orientation foregroundOrient = IdentityOrientation(); // Act @@ -58,12 +58,12 @@ namespace ThreatSource.Tests.Utils public void CalculateProjectedOverlapRatio_ObserverInsideForeground_ReturnsOne() { // Arrange - Vector3D observerPos = new Vector3D(0, 0, 0); // Observer inside foreground - Vector3D backgroundCenter = new Vector3D(0, 0, 10); - Vector3D backgroundDims = new Vector3D(2, 2, 2); + Vector3D observerPos = new(0, 0, 0); // Observer inside foreground + Vector3D backgroundCenter = new(0, 0, 10); + Vector3D backgroundDims = new(2, 2, 2); Orientation backgroundOrient = IdentityOrientation(); - Vector3D foregroundCenter = new Vector3D(0, 0, 0); - Vector3D foregroundDims = new Vector3D(4, 4, 4); // Foreground larger than background + Vector3D foregroundCenter = new(0, 0, 0); + Vector3D foregroundDims = new(4, 4, 4); // Foreground larger than background Orientation foregroundOrient = IdentityOrientation(); // Act @@ -107,12 +107,12 @@ namespace ThreatSource.Tests.Utils public void CalculateProjectedOverlapRatio_IdenticalObjectsAligned_ReturnsOne() { // Arrange - Vector3D observerPos = new Vector3D(0, 0, -10); - Vector3D backgroundCenter = new Vector3D(0, 0, 0); - Vector3D backgroundDims = new Vector3D(2, 2, 2); + Vector3D observerPos = new(0, 0, -10); + Vector3D backgroundCenter = new(0, 0, 0); + Vector3D backgroundDims = new(2, 2, 2); Orientation backgroundOrient = IdentityOrientation(); - Vector3D foregroundCenter = new Vector3D(0, 0, -5); // Foreground between observer and background - Vector3D foregroundDims = new Vector3D(2, 2, 2); // Same size + Vector3D foregroundCenter = new(0, 0, -5); // Foreground between observer and background + Vector3D foregroundDims = new(2, 2, 2); // Same size Orientation foregroundOrient = IdentityOrientation(); // Act @@ -130,17 +130,17 @@ namespace ThreatSource.Tests.Utils public void CalculateProjectedOverlapRatio_PartialOverlap_Horizontal_ReturnsHalf() { // Arrange: Foreground covers half of the background horizontally - Vector3D observerPos = new Vector3D(0, 0, -10); - Vector3D backgroundCenter = new Vector3D(0, 0, 0); + Vector3D observerPos = new(0, 0, -10); + Vector3D backgroundCenter = new(0, 0, 0); // 维度更新为 (宽度 Width=1, 高度 Height=2, 长度 Length=4) - Vector3D backgroundDims = new Vector3D(1.0, 2.0, 4.0); + Vector3D backgroundDims = new(1.0, 2.0, 4.0); Orientation backgroundOrient = IdentityOrientation(); // 前景中心 X 坐标调整为 -0.5,使其投影 U 范围为 [-1.0, 0.0] // 与背景 U 范围 [-0.5, 0.5] 重叠于 [-0.5, 0.0],宽度为 0.5 (背景宽度 1.0 的一半) - Vector3D foregroundCenter = new Vector3D(-0.5, 0.0, -5.0); // X 坐标已调整 + Vector3D foregroundCenter = new(-0.5, 0.0, -5.0); // X 坐标已调整 // 维度更新为 (宽度 Width=1, 高度 Height=2, 长度 Length=2) - Vector3D foregroundDims = new Vector3D(1.0, 2.0, 2.0); + Vector3D foregroundDims = new(1.0, 2.0, 2.0); Orientation foregroundOrient = IdentityOrientation(); // Act @@ -158,17 +158,17 @@ namespace ThreatSource.Tests.Utils { // Arrange // 将观察者移到 Z 轴负方向,确保前景在观察者和背景之间 - var observerPos = new Vector3D(0, 0, -10); - var backgroundCenter = Vector3D.Zero; + Vector3D observerPos = new(0, 0, -10); + Vector3D backgroundCenter = Vector3D.Zero; // 维度现在表示 (宽度 Width=1, 高度 Height=2, 长度 Length=4) - var backgroundDims = new Vector3D(1.0, 2.0, 4.0); - var backgroundOrient = new Orientation(0, 0, 0); + Vector3D backgroundDims = new(1.0, 2.0, 4.0); + Orientation backgroundOrient = new(0, 0, 0); // 前景在背景前面,中心偏移 - var foregroundCenter = new Vector3D(0.0, -1.0, -5.0); + Vector3D foregroundCenter = new(0.0, -1.0, -5.0); // 维度现在表示 (宽度 Width=1, 高度 Height=2, 长度 Length=2) - var foregroundDims = new Vector3D(1.0, 2.0, 2.0); - var foregroundOrient = new Orientation(0, 0, 0); + Vector3D foregroundDims = new(1.0, 2.0, 2.0); + Orientation foregroundOrient = new(0, 0, 0); // Act double ratio = ObscurationUtils.CalculateProjectedOverlapRatio( @@ -184,19 +184,19 @@ namespace ThreatSource.Tests.Utils public void CalculateProjectedOverlapRatio_PartialOverlap_Horizontal_ReturnsThreeQuarters() { // Arrange: 前景覆盖背景宽度的 75% - Vector3D observerPos = new Vector3D(0, 0, -10); - Vector3D backgroundCenter = new Vector3D(0, 0, 0); + Vector3D observerPos = new(0, 0, -10); + Vector3D backgroundCenter = new(0, 0, 0); // 背景宽度 = 4, 高度 = 2 - Vector3D backgroundDims = new Vector3D(4.0, 2.0, 1.0); // W=4, H=2, L=1 + Vector3D backgroundDims = new(4.0, 2.0, 1.0); // W=4, H=2, L=1 Orientation backgroundOrient = new Orientation(0, 0, 0); // 前景宽度 = 3, 高度 = 2, 中心 X 偏移 -0.5 // U 范围: [-0.5 - 1.5, -0.5 + 1.5] = [-2.0, 1.0] // 背景 U 范围: [-2.0, 2.0] // 重叠宽度 = 3.0 - Vector3D foregroundCenter = new Vector3D(-0.5, 0.0, -5.0); - Vector3D foregroundDims = new Vector3D(3.0, 2.0, 1.0); // W=3, H=2, L=1 - Orientation foregroundOrient = new Orientation(0, 0, 0); + Vector3D foregroundCenter = new(-0.5, 0.0, -5.0); + Vector3D foregroundDims = new(3.0, 2.0, 1.0); // W=3, H=2, L=1 + Orientation foregroundOrient = new(0, 0, 0); // Act double ratio = ObscurationUtils.CalculateProjectedOverlapRatio( @@ -214,19 +214,19 @@ namespace ThreatSource.Tests.Utils public void CalculateProjectedOverlapRatio_PartialOverlap_Vertical_ReturnsNineTenths() { // Arrange: 前景覆盖背景高度的 90% - Vector3D observerPos = new Vector3D(0, 0, -10); - Vector3D backgroundCenter = new Vector3D(0, 0, 0); + Vector3D observerPos = new(0, 0, -10); + Vector3D backgroundCenter = new(0, 0, 0); // 背景宽度 = 2, 高度 = 10 - Vector3D backgroundDims = new Vector3D(2.0, 10.0, 1.0); // W=2, H=10, L=1 + Vector3D backgroundDims = new(2.0, 10.0, 1.0); // W=2, H=10, L=1 Orientation backgroundOrient = new Orientation(0, 0, 0); // 前景宽度 = 2, 高度 = 9, 中心 Y 偏移 -0.5 // V 范围: [-0.5 - 4.5, -0.5 + 4.5] = [-5.0, 4.0] // 背景 V 范围: [-5.0, 5.0] // 重叠高度 = 9.0 - Vector3D foregroundCenter = new Vector3D(0.0, -0.5, -5.0); - Vector3D foregroundDims = new Vector3D(2.0, 9.0, 1.0); // W=2, H=9, L=1 - Orientation foregroundOrient = new Orientation(0, 0, 0); + Vector3D foregroundCenter = new(0.0, -0.5, -5.0); + Vector3D foregroundDims = new(2.0, 9.0, 1.0); // W=2, H=9, L=1 + Orientation foregroundOrient = new(0, 0, 0); // Act double ratio = ObscurationUtils.CalculateProjectedOverlapRatio( @@ -244,17 +244,17 @@ namespace ThreatSource.Tests.Utils public void CalculateProjectedOverlapRatio_FarObserver_RuntimeScenario_ShouldBeOne() { // Arrange: 使用运行时观察到的参数 - Vector3D observerPos = new Vector3D(2100.0, 1.2, 0.0); // 远距离观察者 + Vector3D observerPos = new(2100.0, 1.2, 0.0); // 远距离观察者 // 背景 (目标: Tank_1) - Vector3D backgroundCenter = new Vector3D(0.0, 1.2, 0.0); - Vector3D backgroundDims = new Vector3D(3.5, 2.4, 10.0); // W=3.5, H=2.4, L=10.0 - Orientation backgroundOrient = new Orientation(Math.PI/2, 0.0, 0.0); // Yaw=180 deg (原Yaw=PI) + Vector3D backgroundCenter = new(0.0, 1.2, 0.0); + Vector3D backgroundDims = new(3.5, 2.4, 10.0); // W=3.5, H=2.4, L=10.0 + Orientation backgroundOrient = new(Math.PI/2, 0.0, 0.0); // Yaw=180 deg (原Yaw=PI) // 前景 (烟幕: SG_2) - Vector3D foregroundCenter = new Vector3D(50.0, 5.0, 0.0); - Vector3D foregroundDims = new Vector3D(5.0, 10.0, 50.0); - Orientation foregroundOrient = new Orientation(0.0, 0.0, 0.0); + Vector3D foregroundCenter = new(50.0, 5.0, 0.0); + Vector3D foregroundDims = new(5.0, 10.0, 50.0); + Orientation foregroundOrient = new(0.0, 0.0, 0.0); // Act double ratio = ObscurationUtils.CalculateProjectedOverlapRatio( diff --git a/ThreatSource/data/targets/apcs/apc_001.json b/ThreatSource/data/equipments/apcs/apc_001.json similarity index 100% rename from ThreatSource/data/targets/apcs/apc_001.json rename to ThreatSource/data/equipments/apcs/apc_001.json diff --git a/ThreatSource/data/targets/apcs/apc_001.toml b/ThreatSource/data/equipments/apcs/apc_001.toml similarity index 100% rename from ThreatSource/data/targets/apcs/apc_001.toml rename to ThreatSource/data/equipments/apcs/apc_001.toml diff --git a/ThreatSource/data/targets/helicopters/heli_001.json b/ThreatSource/data/equipments/helicopters/heli_001.json similarity index 100% rename from ThreatSource/data/targets/helicopters/heli_001.json rename to ThreatSource/data/equipments/helicopters/heli_001.json diff --git a/ThreatSource/data/targets/helicopters/heli_001.toml b/ThreatSource/data/equipments/helicopters/heli_001.toml similarity index 100% rename from ThreatSource/data/targets/helicopters/heli_001.toml rename to ThreatSource/data/equipments/helicopters/heli_001.toml diff --git a/ThreatSource/data/targets/tanks/mbt_001.json b/ThreatSource/data/equipments/tanks/mbt_001.json similarity index 100% rename from ThreatSource/data/targets/tanks/mbt_001.json rename to ThreatSource/data/equipments/tanks/mbt_001.json diff --git a/ThreatSource/data/targets/tanks/mbt_001.toml b/ThreatSource/data/equipments/tanks/mbt_001.toml similarity index 50% rename from ThreatSource/data/targets/tanks/mbt_001.toml rename to ThreatSource/data/equipments/tanks/mbt_001.toml index c999e12..ba26c81 100644 --- a/ThreatSource/data/targets/tanks/mbt_001.toml +++ b/ThreatSource/data/equipments/tanks/mbt_001.toml @@ -13,7 +13,7 @@ Width = 3.5 # 宽度 (米) Height = 2.4 # 高度 (米) MaxSpeed = 70.0 # 最大速度 (千米/小时) ArmorThickness = 800.0 # 装甲厚度 (毫米) -RadarCrossSection = 15.0 # 雷达散射截面积 (平方米) +RadarCrossSection = 15.0 # 雷达散射截面积 (平方米) - 保留作为通用/回退值 InfraredRadiationIntensity = 250.0 # 红外辐射强度 (瓦特/球面度) UltravioletRadiationIntensity = 15.0 # 紫外辐射强度 (瓦特/球面度) MillimeterWaveRadiationIntensity = 10.0 # 毫米波辐射强度 (瓦特/球面度) @@ -30,4 +30,34 @@ MovingPatternSource = [ # 移动时温度分布 [45, 50, 85], [40, 45, 95], [65, 65, 75] -] \ No newline at end of file +] + +[Properties.RcsPattern] +# RcsBinsSource 是一个 RcsAngularBin 对象的列表 +# 每个对象定义特定角度扇区的RCS值 +# 角度单位:度,RCS单位:dBsm +# HorAngle: 方位角,相对于目标前方 (0度) +# VerAngle: 俯仰角,相对于目标水平面 (0度) + +# 数据基于用户提供的列表,假设非环绕范围的起始角度 < 结束角度 +# 原始数据列: ID, HorStartCol2, RCSdBsmCol3, UnkCol4, HorEndCol5, VerStartCol6, VerEndCol7, AspectCol8 + +# 新的RCS矩阵表示 +# 用户将填充实际的RCS值 (单位: dBsm) +[Properties.RcsPattern.RcsMatrix] +# Data 是一个 6x4 的二维数组 +# 行顺序 (索引 0-5): 前, 后, 左, 右, 上, 下 +# 列顺序 (索引 0-3): 对应每个主方向观察视角下的标准笛卡尔坐标系第1, 2, 3, 4象限 +# 例如,对于"前"方向: +# 列0 (Q1): 右上区域 +# 列1 (Q2): 左上区域 +# 列2 (Q3): 左下区域 +# 列3 (Q4): 右下区域 +Data = [ + [0.0, 0.0, 0.0, 0.0], # 前 (Front) [Q1, Q2, Q3, Q4] + [0.0, 0.0, 0.0, 0.0], # 后 (Back) + [0.0, 0.0, 0.0, 0.0], # 左 (Left) + [0.0, 0.0, 0.0, 0.0], # 右 (Right) + [0.0, 0.0, 0.0, 0.0], # 上 (Top) + [0.0, 0.0, 0.0, 0.0] # 下 (Bottom) +] \ No newline at end of file diff --git a/ThreatSource/src/Data/ThreatSourceDataManager.cs b/ThreatSource/src/Data/ThreatSourceDataManager.cs index 1a77dbe..dfbfe9c 100644 --- a/ThreatSource/src/Data/ThreatSourceDataManager.cs +++ b/ThreatSource/src/Data/ThreatSourceDataManager.cs @@ -31,7 +31,7 @@ namespace ThreatSource.Data private readonly Dictionary _missiles = []; private readonly Dictionary _indicators = []; private readonly Dictionary _sensors = []; - private readonly Dictionary _targets = []; + private readonly Dictionary _equipments = []; private readonly Dictionary _weathers = []; private readonly Dictionary _jammers = []; /// @@ -55,7 +55,7 @@ namespace ThreatSource.Data LoadMissiles(Path.Combine(path, "missiles")); LoadIndicators(Path.Combine(path, "indicators")); LoadSensors(Path.Combine(path, "sensors")); - LoadTargets(Path.Combine(path, "targets")); + LoadTargets(Path.Combine(path, "equipments")); LoadWeathers(Path.Combine(path, "weathers")); LoadJammers(Path.Combine(path, "jammers")); } @@ -177,7 +177,7 @@ namespace ThreatSource.Data if (data != null) { string model = Path.GetFileNameWithoutExtension(file); - _targets[model] = data; + _equipments[model] = data; } } catch (Exception ex) @@ -292,9 +292,9 @@ namespace ThreatSource.Data /// /// 目标型号 /// 目标配置数据 - public EquipmentData GetTarget(string model) + public EquipmentData GetEquipment(string model) { - if (_targets.TryGetValue(model, out var data)) + if (_equipments.TryGetValue(model, out var data)) return data; throw new KeyNotFoundException($"目标 {model} 不存在"); } @@ -342,7 +342,7 @@ namespace ThreatSource.Data /// /// 获取所有可用的目标ID列表 /// - public IEnumerable GetAvailableTargets() => _targets.Keys; + public IEnumerable GetAvailableTargets() => _equipments.Keys; /// /// 获取所有可用的天气ID列表 diff --git a/ThreatSource/src/Data/ThreatSourceFactory.cs b/ThreatSource/src/Data/ThreatSourceFactory.cs index 84aa417..7215b6b 100644 --- a/ThreatSource/src/Data/ThreatSourceFactory.cs +++ b/ThreatSource/src/Data/ThreatSourceFactory.cs @@ -173,9 +173,9 @@ namespace ThreatSource.Data /// 目标型号 /// 初始运动参数 /// 目标实例 - public SimulationElement CreateTarget(string targetId, string targetModel, KinematicState motionParameters) + public SimulationElement CreateEquipment(string targetId, string targetModel, KinematicState motionParameters) { - var data = _dataManager.GetTarget(targetModel); + var data = _dataManager.GetEquipment(targetModel); return data.Type switch { diff --git a/ThreatSource/src/Equipment/BaseEquipment.cs b/ThreatSource/src/Equipment/BaseEquipment.cs index b1f3889..b0e5427 100644 --- a/ThreatSource/src/Equipment/BaseEquipment.cs +++ b/ThreatSource/src/Equipment/BaseEquipment.cs @@ -50,7 +50,6 @@ namespace ThreatSource.Equipment : base(id, motionParameters, simulationManager) { Properties = equipmentProperties; - } /// diff --git a/ThreatSource/src/Equipment/EquipmentProperties.cs b/ThreatSource/src/Equipment/EquipmentProperties.cs index 74bea38..8e16139 100644 --- a/ThreatSource/src/Equipment/EquipmentProperties.cs +++ b/ThreatSource/src/Equipment/EquipmentProperties.cs @@ -1,4 +1,5 @@ using ThreatSource.Utils; +using ThreatSource.Equipment; // Ensure RcsPattern and ThermalPattern are findable namespace ThreatSource.Equipment { @@ -162,6 +163,14 @@ namespace ThreatSource.Equipment /// public ThermalPattern ThermalPattern { get; set; } + /// + /// 获取或设置目标的雷达散射截面(RCS)分布模式 + /// + /// + /// 描述目标的雷达回波特征分布,包含角度依赖的RCS数据。 + /// + public RcsPattern RcsPattern { get; set; } + /// /// 初始化装备属性类的新实例 /// @@ -174,8 +183,11 @@ namespace ThreatSource.Equipment { SetDefaultValues(); Type = "Tank"; // 默认类型为坦克 - // 初始化一个默认的温度分布模式 - ThermalPattern = new ThermalPattern(new double[3, 3], new double[3, 3]); + ThermalPattern = new ThermalPattern( + new double[ThreatSource.Equipment.ThermalPattern.GRID_SIZE, ThreatSource.Equipment.ThermalPattern.GRID_SIZE], + new double[ThreatSource.Equipment.ThermalPattern.GRID_SIZE, ThreatSource.Equipment.ThermalPattern.GRID_SIZE] + ); + RcsPattern = new RcsPattern(); // Initialize with empty list, to be filled by deserializer } /// diff --git a/ThreatSource/src/Equipment/RcsAngularBin.cs b/ThreatSource/src/Equipment/RcsAngularBin.cs new file mode 100644 index 0000000..a837813 --- /dev/null +++ b/ThreatSource/src/Equipment/RcsAngularBin.cs @@ -0,0 +1,102 @@ +using System; + +namespace ThreatSource.Equipment +{ + /// + /// 定义特定角度区间的雷达散射截面(RCS)数据。 + /// + public class RcsAngularBin + { + /// + /// 角度区间描述名称 (如 "前方", "左侧上方")。 + /// + public string AspectName { get; set; } = "Default"; + + /// + /// 水平起始角度 (度)。 + /// + public double HorAngleStartDeg { get; set; } + + /// + /// 水平终止角度 (度)。 + /// + public double HorAngleEndDeg { get; set; } + + /// + /// 俯仰起始角度 (度)。 + /// + public double VerAngleStartDeg { get; set; } + + /// + /// 俯仰终止角度 (度)。 + /// + public double VerAngleEndDeg { get; set; } + + /// + /// 雷达散射截面值 (dBsm)。 + /// + public double RcsValueDbSm { get; set; } + + /// + /// RCS值 (平方米 m^2)。 + /// + public double RcsValueM2 => Math.Pow(10, RcsValueDbSm / 10.0); + + /// + /// 检查给定角度是否在此RCS数据仓内。 + /// + /// 方位角 (度) + /// 俯仰角 (度) + /// 若角度在仓内则为true,否则为false。 + public bool IsAngleInBin(double azimuthDeg, double elevationDeg) + { + bool inHorizontal; + if (HorAngleStartDeg <= HorAngleEndDeg) + { + // 标准区间: [起始, 终止) + inHorizontal = (azimuthDeg >= HorAngleStartDeg && azimuthDeg < HorAngleEndDeg); + } + else + { + // 环绕区间 (例如:350度到10度,表示350-360 或 0-10) + inHorizontal = (azimuthDeg >= HorAngleStartDeg && azimuthDeg < 360.0) || + (azimuthDeg >= 0.0 && azimuthDeg < HorAngleEndDeg); + } + + if (HorAngleStartDeg == HorAngleEndDeg) + { + // 若起始角度等于终止角度,通常表示一个零宽度区间或整个圆周。 + // 当前逻辑下,零宽度区间 (如 [10, 10) ) 不会匹配任何值。 + // 若要匹配精确角度,需调整为 azimuthDeg == HorAngleStartDeg。 + // 若要表示整个圆周 (如 0到0 代表0-360),应定义为 HorAngleStartDeg=0, HorAngleEndDeg=360。 + } + + bool inVertical; + if (VerAngleStartDeg <= VerAngleEndDeg) + { + // 标准俯仰角区间: [起始, 终止) + inVertical = (elevationDeg >= VerAngleStartDeg && elevationDeg < VerAngleEndDeg); + } + else + { + // 俯仰角通常不环绕 (-90 到 +90)。此情况非标准。 + inVertical = false; + } + + if (VerAngleStartDeg == VerAngleEndDeg) + { + // 与水平角类似,零宽度俯仰角区间当前不匹配。 + } + + return inHorizontal && inVertical; + } + + /// + /// 构造函数。 + /// + public RcsAngularBin() + { + AspectName = "Default"; + } + } +} \ No newline at end of file diff --git a/ThreatSource/src/Equipment/RcsPattern.cs b/ThreatSource/src/Equipment/RcsPattern.cs new file mode 100644 index 0000000..68c0278 --- /dev/null +++ b/ThreatSource/src/Equipment/RcsPattern.cs @@ -0,0 +1,405 @@ +using ThreatSource.Utils; // 假设 Vector3D 在此命名空间下 +using System.Linq; // For Average, if used later +using System.Collections.Generic; // Required for List +using System; // Required for ArgumentNullException, ArgumentException, InvalidOperationException +using System.Diagnostics; // Required for Debug.WriteLine + +namespace ThreatSource.Equipment +{ + /// + /// 管理目标的雷达散射截面(RCS)角度依赖数据。 + /// 使用一个6x4矩阵存储RCS值,对应目标的前、后、左、右、上、下六个主方向, + /// 以及每个主方向下根据观察者局部视角划分的四个象限。 + /// + public class RcsPattern + { + private const int RCS_MATRIX_ROWS = 6; + private const int RCS_MATRIX_COLS = 4; + private const double DEFAULT_RCS_DBSM = -20.0; // 示例默认值 -20 dBsm + + /// + /// RCS数据矩阵的源数据 (主要供Tomlyn等TOML解析器使用)。 + /// TOML配置文件中的 'RcsMatrix.Data' 字段将映射到此属性。 + /// 代表一个6x4的RCS值矩阵 (dBsm)。 + /// + public List> RcsMatrixDataSource { get; set; } + + /// + /// RCS数据矩阵 (6x4, 单位 dBsm)。通过按需转换 获取。 + /// 行定义: 0-前, 1-后, 2-左, 3-右, 4-上, 5-下。 + /// 列定义 (象限): 基于观察者相对于当前主方向局部参考轴的左右和上下位置确定。 + /// - 列0: 局部右, 局部上 + /// - 列1: 局部左, 局部上 + /// - 列2: 局部左, 局部下 + /// - 列3: 局部右, 局部下 + /// + public double[,] RcsMatrixData + { + get { return ConvertSourceToGrid(RcsMatrixDataSource, nameof(RcsMatrixDataSource)); } + } + + /// + /// 默认构造函数。初始化一个6x4的RCS数据源,所有值设为默认值 ()。 + /// 主要供Tomlyn等反序列化工具使用。 + /// + public RcsPattern() + { + RcsMatrixDataSource = new List>(RCS_MATRIX_ROWS); + for (int i = 0; i < RCS_MATRIX_ROWS; i++) + { + var row = new List(RCS_MATRIX_COLS); + for (int j = 0; j < RCS_MATRIX_COLS; j++) + { + row.Add(DEFAULT_RCS_DBSM); + } + RcsMatrixDataSource.Add(row); + } + } + + /// + /// 构造函数,用于从外部提供的6x4二维数组初始化RCS模式。 + /// + /// 一个6x4的二维数组,包含RCS值 (dBsm)。 + /// 如果 `rcsMatrix` 为 null。 + /// 如果 `rcsMatrix` 的维度不是6x4。 + public RcsPattern(double[,] rcsMatrix) + { + if (rcsMatrix == null) + { + throw new ArgumentNullException(nameof(rcsMatrix)); + } + if (rcsMatrix.GetLength(0) != RCS_MATRIX_ROWS || rcsMatrix.GetLength(1) != RCS_MATRIX_COLS) + { + throw new ArgumentException($"RCS矩阵的维度必须是 {RCS_MATRIX_ROWS}x{RCS_MATRIX_COLS}。", nameof(rcsMatrix)); + } + RcsMatrixDataSource = ConvertGridToSource(rcsMatrix, nameof(rcsMatrix)); + } + + // 辅助方法:将 List> 转换为 double[,] + private double[,] ConvertSourceToGrid(List> source, string sourceNameForErrorMessage) + { + var grid = new double[RCS_MATRIX_ROWS, RCS_MATRIX_COLS]; + + if (source == null) + { + Debug.WriteLine($"[RcsPattern] Warning: {sourceNameForErrorMessage} is null. Returning default RCS matrix."); + FillWithDefault(grid); + return grid; + } + + if (source.Count != RCS_MATRIX_ROWS) + { + Debug.WriteLine($"[RcsPattern] Warning: {sourceNameForErrorMessage} must have {RCS_MATRIX_ROWS} rows, but found {source.Count}. Returning default RCS matrix."); + FillWithDefault(grid); + return grid; + } + + for (int i = 0; i < RCS_MATRIX_ROWS; i++) + { + if (source[i] == null || source[i].Count != RCS_MATRIX_COLS) + { + Debug.WriteLine($"[RcsPattern] Warning: {sourceNameForErrorMessage} row {i} must have {RCS_MATRIX_COLS} columns, but found {(source[i] == null ? "null" : source[i].Count.ToString())}. Filling row with default and continuing."); + for (int j_fill = 0; j_fill < RCS_MATRIX_COLS; j_fill++) + { + grid[i, j_fill] = DEFAULT_RCS_DBSM; + } + continue; + } + for (int j = 0; j < RCS_MATRIX_COLS; j++) + { + grid[i, j] = source[i][j]; + } + } + return grid; + } + + // 辅助方法:用默认值填充二维数组 + private void FillWithDefault(double[,] gridToFill) + { + for (int i = 0; i < gridToFill.GetLength(0); i++) + { + for (int j = 0; j < gridToFill.GetLength(1); j++) + { + gridToFill[i,j] = DEFAULT_RCS_DBSM; + } + } + } + + // 辅助方法:将 double[,] 转换为 List> + private List> ConvertGridToSource(double[,] grid, string gridNameForErrorMessage) + { + // ArgumentNullException and ArgumentException for grid dimensions are already handled by the constructor using this. + // This method assumes grid is valid. + var source = new List>(RCS_MATRIX_ROWS); + for(int i=0; i < RCS_MATRIX_ROWS; i++) + { + var row = new List(RCS_MATRIX_COLS); + for(int j=0; j < RCS_MATRIX_COLS; j++) + { + row.Add(grid[i,j]); + } + source.Add(row); + } + return source; + } + + /// + /// 根据视线方向的全局方位角和俯仰角获取RCS值 (dBsm)。 + /// + /// 注意:此方法使用简化的、基于全局角度的象限确定逻辑。 + /// 其象限划分可能与向量版本不完全一致,特别是在处理目标后部等复杂情况时。 + /// 对于需要精确局部视角RCS值的场景,推荐使用 版本。 + /// + /// + /// 方位角 (度),相对于目标全局前方参考线 (-180到180度)。 + /// 俯仰角 (度),相对于目标全局水平面 (-90到90度)。 + /// 匹配的RCS值 (dBsm)。 + public double GetRcsValueDbSm(double azimuthDeg, double elevationDeg) + { + // 角度归一化 + double normalizedAzimuthDeg = (azimuthDeg % 360.0); + if (normalizedAzimuthDeg >= 180.0) normalizedAzimuthDeg -= 360.0; + if (normalizedAzimuthDeg < -180.0) normalizedAzimuthDeg += 360.0; + + double normalizedElevationDeg = Math.Max(-90.0, Math.Min(90.0, elevationDeg)); + + int majorAspectRow = -1; + int quadrantColumn = -1; + + var currentRcsMatrix = this.RcsMatrixData; + + if (normalizedElevationDeg > 70) + { + majorAspectRow = 4; // 上 (Top) + if (normalizedAzimuthDeg >= 0 && normalizedAzimuthDeg < 90) quadrantColumn = 0; + else if (normalizedAzimuthDeg >= 90 && normalizedAzimuthDeg < 180) quadrantColumn = 1; + else if (normalizedAzimuthDeg < 0 && normalizedAzimuthDeg > -90) quadrantColumn = 3; + else quadrantColumn = 2; + } + else if (normalizedElevationDeg < -70) + { + majorAspectRow = 5; // 下 (Bottom) + if (normalizedAzimuthDeg >= 0 && normalizedAzimuthDeg < 90) quadrantColumn = 0; + else if (normalizedAzimuthDeg >= 90 && normalizedAzimuthDeg < 180) quadrantColumn = 1; + else if (normalizedAzimuthDeg < 0 && normalizedAzimuthDeg > -90) quadrantColumn = 3; + else quadrantColumn = 2; + } + else + { + if (normalizedAzimuthDeg >= -45 && normalizedAzimuthDeg < 45) majorAspectRow = 0; // 前 (Front) + else if (normalizedAzimuthDeg >= 45 && normalizedAzimuthDeg < 135) majorAspectRow = 3; // 右 (Right) + else if (normalizedAzimuthDeg >= 135 || normalizedAzimuthDeg < -135) majorAspectRow = 1; // 后 (Back) + else majorAspectRow = 2; // 左 (Left) + + bool isTopQuadrant = normalizedElevationDeg >= 0; + + if (majorAspectRow == 0) // 前 + { + if (normalizedAzimuthDeg >= 0) + quadrantColumn = isTopQuadrant ? 0 : 3; + else + quadrantColumn = isTopQuadrant ? 1 : 2; + } + else if (majorAspectRow == 1) // 后 + { + // Simplified logic for 'Back' aspect quadrants + // 后部象限的简化逻辑:基于观察者全局方位角判断左右,可能与目标局部左右不完全对应 + if (normalizedAzimuthDeg > 0) + quadrantColumn = isTopQuadrant ? 0 : 3; + else + quadrantColumn = isTopQuadrant ? 1 : 2; + } + else if (majorAspectRow == 3) // 右 + { + if (normalizedAzimuthDeg < 90) + quadrantColumn = isTopQuadrant ? 0 : 3; + else + quadrantColumn = isTopQuadrant ? 1 : 2; + } + else if (majorAspectRow == 2) // 左 + { + if (normalizedAzimuthDeg < -90) + quadrantColumn = isTopQuadrant ? 0 : 3; + else + quadrantColumn = isTopQuadrant ? 1 : 2; + } + } + + if (majorAspectRow >= 0 && majorAspectRow < RCS_MATRIX_ROWS && quadrantColumn >= 0 && quadrantColumn < RCS_MATRIX_COLS) + { + return currentRcsMatrix[majorAspectRow, quadrantColumn]; + } + else + { + Debug.WriteLine($"[RcsPattern] Could not determine exact cell for az:{azimuthDeg}, el:{elevationDeg}. NormAz:{normalizedAzimuthDeg}, NormEl:{normalizedElevationDeg}. Row:{majorAspectRow}, Col:{quadrantColumn}. Returning default."); + return DEFAULT_RCS_DBSM; + } + } + + /// + /// 根据目标自身坐标系中的观察向量计算精确的局部视角RCS值 (dBsm)。 + /// + /// 此方法通过计算观察者相对于目标各主方向局部参考轴的精确几何关系来确定RCS矩阵的行和列, + /// 提供了比基于全局角度的重载方法 更精确的局部视角RCS值。 + /// + /// + /// 观察者方向向量 (从目标指向观察者,在目标坐标系中,将被归一化)。 + /// 目标"前方"向量 (在目标坐标系中,通常为(1,0,0)或(0,0,1)等)。 + /// 目标"上方"向量 (在目标坐标系中,通常为(0,1,0)或(0,0,1)等)。 + /// 目标\"右方\"向量 (在目标坐标系中)。 + /// RCS值 (dBsm)。 + public double GetRcsValueDbSm(Vector3D observerDirectionInTargetFrame, Vector3D targetForwardVectorInTargetFrame, Vector3D targetUpVectorInTargetFrame, Vector3D targetRightVectorInTargetFrame) + { + Vector3D observerDirNorm = observerDirectionInTargetFrame.Normalize(); + Vector3D upNorm = targetUpVectorInTargetFrame.Normalize(); + Vector3D forwardNorm = targetForwardVectorInTargetFrame.Normalize(); + Vector3D rightNorm = targetRightVectorInTargetFrame.Normalize(); + + // 步骤 1: 从观察向量计算全局方位角和俯仰角,用于确定主方向(行)。 + double elevationAngleRad = Math.Asin(Vector3D.DotProduct(observerDirNorm, upNorm)); + + Vector3D projectionOnHorizontalPlane = (observerDirNorm - upNorm * Math.Sin(elevationAngleRad)); + // 只有在投影向量非零时才进行归一化和方位角计算 + double azimuthAngleRad; + if (projectionOnHorizontalPlane.MagnitudeSquared() < 1e-12) + { + azimuthAngleRad = 0.0; + } + else + { + projectionOnHorizontalPlane = projectionOnHorizontalPlane.Normalize(); + double cosAzimuth = Vector3D.DotProduct(projectionOnHorizontalPlane, forwardNorm); + cosAzimuth = Math.Max(-1.0, Math.Min(1.0, cosAzimuth)); // Clamp value to avoid Acos domain error + azimuthAngleRad = Math.Acos(cosAzimuth); + + if (Vector3D.DotProduct(projectionOnHorizontalPlane, rightNorm) < 0) + { + azimuthAngleRad = -azimuthAngleRad; + } + } + + double globalElevationDeg = elevationAngleRad * (180.0 / Math.PI); + double globalAzimuthDeg = azimuthAngleRad * (180.0 / Math.PI); + + // 角度归一化 (与角度版本方法一致) + double normalizedGlobalAzimuthDeg = (globalAzimuthDeg % 360.0); + if (normalizedGlobalAzimuthDeg >= 180.0) normalizedGlobalAzimuthDeg -= 360.0; + if (normalizedGlobalAzimuthDeg < -180.0) normalizedGlobalAzimuthDeg += 360.0; + double normalizedGlobalElevationDeg = Math.Max(-90.0, Math.Min(90.0, globalElevationDeg)); + + var currentRcsMatrix = this.RcsMatrixData; + int majorAspectRow; + + // 步骤 2: 根据全局角度确定主方向行 (majorAspectRow)。 + if (normalizedGlobalElevationDeg > 70) + { + majorAspectRow = 4; // 上 (Top) + } + else if (normalizedGlobalElevationDeg < -70) + { + majorAspectRow = 5; // 下 (Bottom) + } + else + { + if (normalizedGlobalAzimuthDeg >= -45 && normalizedGlobalAzimuthDeg < 45) majorAspectRow = 0; // 前 (Front) + else if (normalizedGlobalAzimuthDeg >= 45 && normalizedGlobalAzimuthDeg < 135) majorAspectRow = 3; // 右 (Right) + else if (normalizedGlobalAzimuthDeg >= 135 || normalizedGlobalAzimuthDeg < -135) majorAspectRow = 1; // 后 (Back) + else majorAspectRow = 2; // 左 (Left) + } + + // 步骤 3: 定义选定主方向的局部参考轴。 + Vector3D localRight, localUp = upNorm; + + switch (majorAspectRow) + { + case 0: // 前 (Front) + localRight = rightNorm; + break; + case 1: // 后 (Back) + localRight = -rightNorm; + break; + case 2: // 左 (Left) + localRight = forwardNorm; + break; + case 3: // 右 (Right) + localRight = -forwardNorm; + break; + case 4: // 上 (Top) + localRight = rightNorm; + localUp = -forwardNorm; + break; + case 5: // 下 (Bottom) + localRight = rightNorm; + localUp = forwardNorm; + break; + default: // Should not happen + Debug.WriteLine($"[RcsPattern] Invalid majorAspectRow: {majorAspectRow}. Returning default RCS."); + return DEFAULT_RCS_DBSM; + } + + // 步骤 4: 计算观察者相对于局部参考轴的方位,以确定象限列。 + bool isLocallyTopQuadrant; + if (majorAspectRow == 4 || majorAspectRow == 5) // 顶面或底面视角 + { + // 对于顶/底面,局部"上"象限定义为观察者在目标的局部上方向 (localUp)。 + // localUp 对于 Top 是 -forwardNorm,对于 Bottom 是 forwardNorm。 + isLocallyTopQuadrant = Vector3D.DotProduct(observerDirNorm, localUp) >= 0; // MODIFIED: was forwardNorm + } + else // 前/后/左/右视角 + { + // 对于侧面,局部"上"象限定义为观察者在目标的全局上方向 (upNorm)。 + isLocallyTopQuadrant = Vector3D.DotProduct(observerDirNorm, localUp) >= 0; // localUp此时为全局upNorm + } + + // 局部"右"象限定义为观察者在当前主方向的局部右轴 (localRight) 方向。 + bool isLocallyRightQuadrant = Vector3D.DotProduct(observerDirNorm, localRight) >= 0; + + // 步骤 5: 根据局部方位确定象限列 (quadrantColumn)。 + // 象限定义: (0:右上, 1:左上, 2:左下, 3:右下) 从观察者视角看当前主平面。 + int quadrantColumn; + if (isLocallyRightQuadrant) + { + quadrantColumn = isLocallyTopQuadrant ? 0 : 3; + } + else + { + quadrantColumn = isLocallyTopQuadrant ? 1 : 2; + } + + if (majorAspectRow >= 0 && majorAspectRow < RCS_MATRIX_ROWS && quadrantColumn >= 0 && quadrantColumn < RCS_MATRIX_COLS) + { + return currentRcsMatrix[majorAspectRow, quadrantColumn]; + } + else + { + Debug.WriteLine($"[RcsPattern] Vector LJS: Could not determine exact cell for obs: {observerDirectionInTargetFrame}. GlobAz:{globalAzimuthDeg:F1},GlobEl:{globalElevationDeg:F1}. Row:{majorAspectRow}, Col:{quadrantColumn}. Returning default."); + return DEFAULT_RCS_DBSM; + } + } + + /// + /// 计算RCS矩阵中所有值的算术平均值。 + /// + /// 平均RCS值 (dBsm)。 + public double GetAverageRcsDbSm() + { + var currentRcsMatrix = this.RcsMatrixData; + if (currentRcsMatrix == null) + { + return DEFAULT_RCS_DBSM; + } + + double sum = 0; + int count = 0; + for (int i = 0; i < currentRcsMatrix.GetLength(0); i++) + { + for (int j = 0; j < currentRcsMatrix.GetLength(1); j++) + { + sum += currentRcsMatrix[i, j]; + count++; + } + } + return count > 0 ? sum / count : DEFAULT_RCS_DBSM; + } + } +} \ No newline at end of file diff --git a/ThreatSource/src/Guidance/BaseGuidanceSystem.cs b/ThreatSource/src/Guidance/BaseGuidanceSystem.cs index b82c458..e9366a4 100644 --- a/ThreatSource/src/Guidance/BaseGuidanceSystem.cs +++ b/ThreatSource/src/Guidance/BaseGuidanceSystem.cs @@ -331,7 +331,7 @@ namespace ThreatSource.Guidance if (parameters.Mode == JammingMode.Blocking) { HasGuidance = false; // 硬干扰立即停止制导 - Trace.TraceInformation($"[{this.GetType().Name}] 硬干扰已应用: {parameters.Type}"); + Trace.TraceInformation($"[{GetType().Name}] 硬干扰已应用: {parameters.Type}"); } } diff --git a/ThreatSource/src/Guidance/InfraredCommandGuidanceSystem.cs b/ThreatSource/src/Guidance/InfraredCommandGuidanceSystem.cs index 8442355..fcdd98a 100644 --- a/ThreatSource/src/Guidance/InfraredCommandGuidanceSystem.cs +++ b/ThreatSource/src/Guidance/InfraredCommandGuidanceSystem.cs @@ -182,13 +182,13 @@ namespace ThreatSource.Guidance return; } - Vector3D currentDesiredDirection = (LastTrackerToTargetVector - LastTrackerToMissileVector).Normalize(); + Vector3D currentDesiredDirection = (LastTrackerToTargetVector.Value - LastTrackerToMissileVector.Value).Normalize(); Vector3D currentDirection = KState.Velocity.Normalize(); if (LastDesiredDirection != null && deltaTime > 0) { - double instantTurnRate = Vector3D.CrossProduct(LastDesiredDirection, currentDesiredDirection).Magnitude() / deltaTime; + double instantTurnRate = Vector3D.CrossProduct(LastDesiredDirection.Value, currentDesiredDirection).Magnitude() / deltaTime; turnRate = turnRate * (1 - TurnRateSmoothingFactor) + instantTurnRate * TurnRateSmoothingFactor; } else @@ -196,7 +196,7 @@ namespace ThreatSource.Guidance turnRate = 0; } - Vector3D leadDirection = Vector3D.CrossProduct(currentDesiredDirection, Vector3D.CrossProduct(currentDesiredDirection, currentDirection).Normalize()); + Vector3D leadDirection = Vector3D.CrossProduct(currentDesiredDirection, Vector3D.CrossProduct(currentDesiredDirection, currentDirection).Normalize()).Normalize(); Vector3D desiredDirectionWithLead = (currentDesiredDirection + leadDirection * turnRate * LeadTimeFactor).Normalize(); Vector3D turnAxis = Vector3D.CrossProduct(currentDirection, desiredDirectionWithLead).Normalize(); double turnAngle = Vector3D.AngleBetween(currentDirection, desiredDirectionWithLead); @@ -222,7 +222,7 @@ namespace ThreatSource.Guidance // 添加角度信息 if (LastTrackerToTargetVector != null && LastTrackerToMissileVector != null) { - double angle = Vector3D.AngleBetween(LastTrackerToTargetVector, LastTrackerToMissileVector); + double angle = Vector3D.AngleBetween(LastTrackerToTargetVector.Value, LastTrackerToMissileVector.Value); statusInfo.ExtendedProperties["TurnAngle"] = angle; } else diff --git a/ThreatSource/src/Guidance/InfraredImagingGuidanceSystem.cs b/ThreatSource/src/Guidance/InfraredImagingGuidanceSystem.cs index 6adfcce..8cfc2d7 100644 --- a/ThreatSource/src/Guidance/InfraredImagingGuidanceSystem.cs +++ b/ThreatSource/src/Guidance/InfraredImagingGuidanceSystem.cs @@ -494,7 +494,7 @@ namespace ThreatSource.Guidance const double ObscurationThreshold = 0.8; // 80%遮挡视为完全遮挡 Vector3D targetCenter = target.KState.Position; Orientation targetOrient = target.KState.Orientation; - Vector3D targetDims = new Vector3D(target.Properties.Width, target.Properties.Height, target.Properties.Length); + Vector3D targetDims = new(target.Properties.Width, target.Properties.Height, target.Properties.Length); foreach (var smoke in smokeGrenades) { @@ -580,7 +580,7 @@ namespace ThreatSource.Guidance public override ElementStatusInfo GetStatusInfo() { var statusInfo = base.GetStatusInfo(); - string lastPosStr = lastTargetPosition != null ? lastTargetPosition.ToString() : "null"; + string lastPosStr = lastTargetPosition.HasValue ? lastTargetPosition.Value.ToString() : "null"; statusInfo.ExtendedProperties["lastPosStr"] = lastPosStr; return statusInfo; } diff --git a/ThreatSource/src/Guidance/LaserBeamRiderGuidanceSystem.cs b/ThreatSource/src/Guidance/LaserBeamRiderGuidanceSystem.cs index 564c28b..edad608 100644 --- a/ThreatSource/src/Guidance/LaserBeamRiderGuidanceSystem.cs +++ b/ThreatSource/src/Guidance/LaserBeamRiderGuidanceSystem.cs @@ -407,7 +407,7 @@ namespace ThreatSource.Guidance Debug.WriteLine($"激光驾束引导系统: 在控制场内, 距离: {shortestDistance}"); - Vector3D missileToSource = LaserSourcePosition - KState.Position; + Vector3D missileToSource = LaserSourcePosition.Value - KState.Position; double distance = missileToSource.Magnitude(); double receivedPower = CalculateReceivedLaserPower(distance); @@ -510,7 +510,7 @@ namespace ThreatSource.Guidance lateralAcceleration = lateralAcceleration.Normalize() * config.MaxGuidanceAcceleration; } - Vector3D forwardDirection = LaserDirection; + Vector3D forwardDirection = LaserDirection.Value; Vector3D currentDirection = KState.Velocity.Normalize(); Vector3D rotationAxis = Vector3D.CrossProduct(currentDirection, forwardDirection); double rotationAngle = Math.Acos(Vector3D.DotProduct(currentDirection, forwardDirection)); @@ -589,8 +589,8 @@ namespace ThreatSource.Guidance public override ElementStatusInfo GetStatusInfo() { var statusInfo = base.GetStatusInfo(); - string sourcePosStr = LaserSourcePosition != null ? LaserSourcePosition.ToString() : "null"; - string directionStr = LaserDirection != null ? LaserDirection.ToString() : "null"; + string sourcePosStr = LaserSourcePosition.HasValue ? LaserSourcePosition.Value.ToString() : "null"; + string directionStr = LaserDirection.HasValue ? LaserDirection.Value.ToString() : "null"; statusInfo.ExtendedProperties["SourcePos"] = sourcePosStr; statusInfo.ExtendedProperties["Direction"] = directionStr; @@ -618,9 +618,9 @@ namespace ThreatSource.Guidance return Vector3D.Zero; } - Vector3D missileToSource = LaserSourcePosition - KState.Position; - double projectionLength = Vector3D.DotProduct(missileToSource, LaserDirection); - Vector3D closestPointOnBeam = LaserSourcePosition - LaserDirection * projectionLength; + Vector3D missileToSource = LaserSourcePosition.Value - KState.Position; + double projectionLength = Vector3D.DotProduct(missileToSource, LaserDirection.Value); + Vector3D closestPointOnBeam = LaserSourcePosition.Value - LaserDirection.Value * projectionLength; Vector3D shortestDistanceVector = closestPointOnBeam - KState.Position; return shortestDistanceVector; diff --git a/ThreatSource/src/Guidance/LaserSemiActiveGuidanceSystem.cs b/ThreatSource/src/Guidance/LaserSemiActiveGuidanceSystem.cs index 94d966c..f089d66 100644 --- a/ThreatSource/src/Guidance/LaserSemiActiveGuidanceSystem.cs +++ b/ThreatSource/src/Guidance/LaserSemiActiveGuidanceSystem.cs @@ -253,7 +253,7 @@ namespace ThreatSource.Guidance { laserTargets.RemoveAt(existingIndex); } - laserTargets.Add((target, evt.SpotPosition, laserDesignator)); + laserTargets.Add((target, evt.SpotPosition.Value, laserDesignator)); // 只要收到有效事件就认为照射开启 (有效性由后续SNR判断) LaserIlluminationOn = true; @@ -590,7 +590,7 @@ namespace ThreatSource.Guidance } // No cast needed, just use TargetPosition directly - Vector3D idealDirection = (TargetPosition - KState.Position).Normalize(); + Vector3D idealDirection = (TargetPosition.Value - KState.Position).Normalize(); // 计算当前导弹前向方向 Vector3D currentDirection = KState.Velocity.Normalize(); diff --git a/ThreatSource/src/Guidance/MillimeterWaveGuidanceSystem.cs b/ThreatSource/src/Guidance/MillimeterWaveGuidanceSystem.cs index 72ea37e..b7b7bf1 100644 --- a/ThreatSource/src/Guidance/MillimeterWaveGuidanceSystem.cs +++ b/ThreatSource/src/Guidance/MillimeterWaveGuidanceSystem.cs @@ -572,7 +572,7 @@ namespace ThreatSource.Guidance if (LastTargetPosition != null) { double proximityThreshold = (target.Properties.Length > 0) ? target.Properties.Length * 5.0 : 100.0; - isPotentiallyTrackedTarget = Vector3D.Distance(target.KState.Position, LastTargetPosition) < proximityThreshold; + isPotentiallyTrackedTarget = Vector3D.Distance(target.KState.Position, LastTargetPosition.Value) < proximityThreshold; } if (IsTargetInBeam(missileVelocity, toTarget) && isPotentiallyTrackedTarget) @@ -664,8 +664,8 @@ namespace ThreatSource.Guidance { var statusInfo = base.GetStatusInfo(); - string lastPosStr = LastTargetPosition != null ? LastTargetPosition.ToString() : "null"; - string lastVelStr = LastTargetVelocity != null ? LastTargetVelocity.ToString() : "null"; + string lastPosStr = LastTargetPosition.HasValue ? LastTargetPosition.Value.ToString() : "null"; + string lastVelStr = LastTargetVelocity.HasValue ? LastTargetVelocity.Value.ToString() : "null"; statusInfo.ExtendedProperties["Mode"] = currentMode.ToString(); statusInfo.ExtendedProperties["LastTargetPosition"] = lastPosStr; @@ -723,7 +723,7 @@ namespace ThreatSource.Guidance weatherSpecificAtmosphericTransmittance = AtmosphereDllWrapper.CalculateTransmittance( distance, RadiationType.MillimeterWave, - wavelength_um, // Use wavelength_um + wavelength_um, SimulationManager.CurrentWeather); } signalPower *= weatherSpecificAtmosphericTransmittance; @@ -738,18 +738,13 @@ namespace ThreatSource.Guidance double snr_dB_no_smoke = 10 * Math.Log10(snr_linear); Debug.WriteLine($"[SNR计算] SNR_dB (烟幕前): {snr_dB_no_smoke:F2} dB. Incoming SmokeTransmittance_Linear: {smokeTransmittanceLinear:F6}"); - // 应用当前生效的烟幕衰减 (通过存储的透过率) - REMOVED _currentSmokeTransmittance FIELD USAGE - // signalPower *= _currentSmokeTransmittance; // This line is now handled by the parameter - - // 应用烟幕透过率 (smokeTransmittanceLinear is already linear 0-1) - if (smokeTransmittanceLinear <= 0.000001) // Threshold to prevent log(0) or very large negative numbers + // 应用烟幕透过率 + if (smokeTransmittanceLinear <= 1e-6) // 防止log(0)或非常大的负数 { Debug.WriteLine($"[SNR计算] 烟幕透过率太低. Final_SNR_dB: -100.0 dB"); - return -100.0; // Effectively zero signal due to smoke + return -100.0; // 由于烟幕影响,信号实际上为零 } - // Loss_smoke_dB = -10 * log10(T_smoke_linear) - // SNR_final_dB = SNR_original_dB - Loss_smoke_dB = SNR_original_dB + 10 * log10(T_smoke_linear) double smokeEffect_dB = 10 * Math.Log10(smokeTransmittanceLinear); double final_snr_dB = snr_dB_no_smoke + smokeEffect_dB; Debug.WriteLine($"[SNR计算] 烟幕影响: {smokeEffect_dB:F2} dB. 最终SNR_dB (烟幕后): {final_snr_dB:F2} dB"); @@ -793,7 +788,7 @@ namespace ThreatSource.Guidance double transmittanceForThisSmoke = smokeGrenade.GetSmokeTransmittanceOnLine(observerPosition, targetEndPosition, wavelength_um); totalTransmittance *= transmittanceForThisSmoke; - if (totalTransmittance < 0.000001) // 使用更小的阈值提前退出,如果透过率几乎为零 + if (totalTransmittance < 1e-6) // 使用更小的阈值提前退出,如果透过率几乎为零 { return 0.0; } diff --git a/ThreatSource/src/Indicator/BaseIndicator.cs b/ThreatSource/src/Indicator/BaseIndicator.cs index 3d8fcf5..611017d 100644 --- a/ThreatSource/src/Indicator/BaseIndicator.cs +++ b/ThreatSource/src/Indicator/BaseIndicator.cs @@ -158,7 +158,7 @@ namespace ThreatSource.Indicator /// 干扰参数 protected virtual void HandleJammingApplied(JammingParameters parameters) { - Debug.WriteLine($"[BaseIndicator] {this.GetType().Name} {Id} 接收到应用干扰事件: {parameters.Type}, 功率: {parameters.Power}W", "Jamming"); + Debug.WriteLine($"[BaseIndicator] {GetType().Name} {Id} 接收到应用干扰事件: {parameters.Type}, 功率: {parameters.Power}W", "Jamming"); } /// @@ -167,7 +167,7 @@ namespace ThreatSource.Indicator /// 干扰参数 protected virtual void HandleJammingCleared(JammingParameters parameters) { - Debug.WriteLine($"[BaseIndicator] {this.GetType().Name} {Id} 接收到清除干扰事件: {parameters.Type}", "Jamming"); + Debug.WriteLine($"[BaseIndicator] {GetType().Name} {Id} 接收到清除干扰事件: {parameters.Type}", "Jamming"); } /// @@ -403,7 +403,7 @@ namespace ThreatSource.Indicator protected virtual bool ShouldHandleJamming(JammingParameters parameters) { // 首先检查当前指示器实例(通过其具体的 SupportedJammingTypes 实现)是否支持此干扰类型 - if (!this.SupportedJammingTypes.Contains(parameters.Type)) + if (!SupportedJammingTypes.Contains(parameters.Type)) { return false; } diff --git a/ThreatSource/src/Indicator/InfraredTracker.cs b/ThreatSource/src/Indicator/InfraredTracker.cs index 28639c2..5434896 100644 --- a/ThreatSource/src/Indicator/InfraredTracker.cs +++ b/ThreatSource/src/Indicator/InfraredTracker.cs @@ -131,7 +131,7 @@ namespace ThreatSource.Indicator // 目标被遮挡,尝试使用最后已知位置 if (_lastKnownTargetPosition != null) { - currentTargetPosition = _lastKnownTargetPosition; + currentTargetPosition = _lastKnownTargetPosition.Value; Debug.WriteLine($"红外测角仪 {Id}: 目标被遮挡,使用最后已知位置 {currentTargetPosition}"); } else diff --git a/ThreatSource/src/Jammer/SmokeGrenade.cs b/ThreatSource/src/Jammer/SmokeGrenade.cs index 83465a5..b366381 100644 --- a/ThreatSource/src/Jammer/SmokeGrenade.cs +++ b/ThreatSource/src/Jammer/SmokeGrenade.cs @@ -353,7 +353,7 @@ namespace ThreatSource.Jammer Vector3D.DotProduct(rayOriginRelativeToOBBCenter_w, obbAxisZ_w) ); - Vector3D rayDirection_local = new Vector3D( + Vector3D rayDirection_local = new( Vector3D.DotProduct(rayDirection_world, obbAxisX_w), Vector3D.DotProduct(rayDirection_world, obbAxisY_w), Vector3D.DotProduct(rayDirection_world, obbAxisZ_w) diff --git a/ThreatSource/src/MIssile/InfraredCommandGuidedMissile.cs b/ThreatSource/src/MIssile/InfraredCommandGuidedMissile.cs index 6d05a97..41cfa0f 100644 --- a/ThreatSource/src/MIssile/InfraredCommandGuidedMissile.cs +++ b/ThreatSource/src/MIssile/InfraredCommandGuidedMissile.cs @@ -206,7 +206,7 @@ namespace ThreatSource.Missile /// public void LightInfraredSource() { - SimulationManager.PublishEvent(new InfraredGuidanceMissileLightEvent { RadiationPower = this.RadiationPower, SenderId = this.Id }); + SimulationManager.PublishEvent(new InfraredGuidanceMissileLightEvent { RadiationPower = RadiationPower, SenderId = Id }); } /// @@ -232,7 +232,7 @@ namespace ThreatSource.Missile /// public void LightOffInfraredSource() { - SimulationManager.PublishEvent(new InfraredGuidanceMissileLightOffEvent { SenderId = this.Id }); + SimulationManager.PublishEvent(new InfraredGuidanceMissileLightOffEvent { SenderId = Id }); } /// diff --git a/ThreatSource/src/MIssile/TerminalSensitiveMissile.cs b/ThreatSource/src/MIssile/TerminalSensitiveMissile.cs index 5d24074..06c0bab 100644 --- a/ThreatSource/src/MIssile/TerminalSensitiveMissile.cs +++ b/ThreatSource/src/MIssile/TerminalSensitiveMissile.cs @@ -144,7 +144,7 @@ namespace ThreatSource.Missile //计算此时目标的预期位置 Vector3D targetPredictPosition = target.KState.Position + target.KState.Velocity * timeToScanPoint; //计算分离点位置 - separationPoint = Vector3D.PointOnLine(targetPredictPosition, base.KState.Position, this.submunitionConfig.SeparationDistance) + new Vector3D(0, this.submunitionConfig.SeparationHeight, 0); + separationPoint = Vector3D.PointOnLine(targetPredictPosition, base.KState.Position, submunitionConfig.SeparationDistance) + new Vector3D(0, submunitionConfig.SeparationHeight, 0); // 获取风速 Vector3D windVec = Vector3D.Zero; @@ -170,8 +170,8 @@ namespace ThreatSource.Missile } else { - KState.Velocity = launchVelocityNullable; - KState.Orientation = Orientation.FromVector(launchVelocityNullable); + KState.Velocity = launchVelocityNullable.Value; + KState.Orientation = Orientation.FromVector(launchVelocityNullable.Value); } } @@ -316,7 +316,7 @@ namespace ThreatSource.Missile Vector3D scanTargetPosition = targetPredictPosition + new Vector3D(0, submunitionConfig.StableScanHeight, 0); // 计算子弹的初始方向 - Vector3D? initialDirection = MotionAlgorithm.CalculateLaunchVelocityWithSimplifiedDrag( + Vector3D? initialDirectionNullable = MotionAlgorithm.CalculateLaunchVelocityWithSimplifiedDrag( KState.Position, scanTargetPosition, KState.Speed, @@ -325,14 +325,12 @@ namespace ThreatSource.Missile null ); - if (initialDirection == null) + if (initialDirectionNullable == null) { throw new InvalidOperationException("无法计算子弹的初始方向"); } - else - { - initialDirection = initialDirection.Normalize(); - } + + Vector3D initialDirection = initialDirectionNullable.Value.Normalize(); // 根据新方向创建朝向 Orientation separationOrientation = Orientation.FromVector(initialDirection); diff --git a/ThreatSource/src/Sensor/InfraredDetector.cs b/ThreatSource/src/Sensor/InfraredDetector.cs index aadce93..6a2f1f4 100644 --- a/ThreatSource/src/Sensor/InfraredDetector.cs +++ b/ThreatSource/src/Sensor/InfraredDetector.cs @@ -117,7 +117,7 @@ namespace ThreatSource.Sensor var wavelength = config.Band == InfraredBand.Long ? 10.0 : 4.0; // 代表性波长 // 计算此烟幕的透射率 - double singleSmokeTransmittance = smokeGrenade.GetSmokeTransmittanceOnLine(KState.Position, targetPointForLos, wavelength); + double singleSmokeTransmittance = smokeGrenade.GetSmokeTransmittanceOnLine(KState.Position, targetPointForLos.Value, wavelength); smokeAttenuation *= singleSmokeTransmittance; // 累乘透射率 } } diff --git a/ThreatSource/src/Sensor/LaserRangefinder.cs b/ThreatSource/src/Sensor/LaserRangefinder.cs index 01903d0..3e7217f 100644 --- a/ThreatSource/src/Sensor/LaserRangefinder.cs +++ b/ThreatSource/src/Sensor/LaserRangefinder.cs @@ -123,7 +123,7 @@ namespace ThreatSource.Sensor var wavelength = config.Wavelength; // 计算此烟幕的透射率 - double singleSmokeTransmittance = smokeGrenade.GetSmokeTransmittanceOnLine(KState.Position, targetPointForLos, wavelength); + double singleSmokeTransmittance = smokeGrenade.GetSmokeTransmittanceOnLine(KState.Position, targetPointForLos.Value, wavelength); smokeAttenuation *= singleSmokeTransmittance; // 累乘透射率 } } @@ -260,9 +260,9 @@ namespace ThreatSource.Sensor if (targetPoint != null) { // 计算到目标点的斜距 - double dx = targetPoint.X - KState.Position.X; - double dy = targetPoint.Y - KState.Position.Y; - double dz = targetPoint.Z - KState.Position.Z; + double dx = targetPoint.Value.X - KState.Position.X; + double dy = targetPoint.Value.Y - KState.Position.Y; + double dz = targetPoint.Value.Z - KState.Position.Z; double calculatedSlantRange = Math.Sqrt(dx * dx + dy * dy + dz * dz); // 确保距离不超过最大探测范围 diff --git a/ThreatSource/src/Sensor/MillimeterWaveAltimeter.cs b/ThreatSource/src/Sensor/MillimeterWaveAltimeter.cs index 513e20d..6463958 100644 --- a/ThreatSource/src/Sensor/MillimeterWaveAltimeter.cs +++ b/ThreatSource/src/Sensor/MillimeterWaveAltimeter.cs @@ -149,9 +149,9 @@ namespace ThreatSource.Sensor if (SimulationManager.GetEntityById(jammerId) is SmokeGrenade smokeGrenade) { // 计算视线目标点 (地面交点) - var targetPointForLos = MotionAlgorithm.CalculateIntersectionWithGround(KState.Position, KState.Orientation.ToVector(), 0); + Vector3D? targetPointForLos = MotionAlgorithm.CalculateIntersectionWithGround(KState.Position, KState.Orientation.ToVector(), 0); - if (targetPointForLos != null) + if (targetPointForLos.HasValue) { var wavelength = config.Band switch { @@ -160,7 +160,7 @@ namespace ThreatSource.Sensor _ => 0.0 }; // 计算此烟幕的透射率 - double singleSmokeTransmittance = smokeGrenade.GetSmokeTransmittanceOnLine(KState.Position, targetPointForLos, wavelength); + double singleSmokeTransmittance = smokeGrenade.GetSmokeTransmittanceOnLine(KState.Position, targetPointForLos.Value, wavelength); smokeAttenuation *= singleSmokeTransmittance; // 累乘透射率 } } diff --git a/ThreatSource/src/Sensor/MillimeterWaveRadiometer.cs b/ThreatSource/src/Sensor/MillimeterWaveRadiometer.cs index e48f7b3..df74a2f 100644 --- a/ThreatSource/src/Sensor/MillimeterWaveRadiometer.cs +++ b/ThreatSource/src/Sensor/MillimeterWaveRadiometer.cs @@ -162,9 +162,9 @@ namespace ThreatSource.Sensor if (SimulationManager.GetEntityById(jammerId) is SmokeGrenade smokeGrenade) { // 计算视线目标点 (地面交点) - var targetPointForLos = MotionAlgorithm.CalculateIntersectionWithGround(KState.Position, KState.Orientation.ToVector(), 0); + Vector3D? targetPointForLos = MotionAlgorithm.CalculateIntersectionWithGround(KState.Position, KState.Orientation.ToVector(), 0); - if (targetPointForLos != null) + if (targetPointForLos.HasValue) { var wavelength = config.Band switch { @@ -173,7 +173,7 @@ namespace ThreatSource.Sensor _ => 0.0 }; // 计算此烟幕的透射率 - double singleSmokeTransmittance = smokeGrenade.GetSmokeTransmittanceOnLine(KState.Position, targetPointForLos, wavelength); + double singleSmokeTransmittance = smokeGrenade.GetSmokeTransmittanceOnLine(KState.Position, targetPointForLos.Value, wavelength); smokeAttenuation *= singleSmokeTransmittance; // 累乘透射率 } } diff --git a/ThreatSource/src/Sensor/QuadrantDetector.cs b/ThreatSource/src/Sensor/QuadrantDetector.cs index 34a1fc5..660e738 100644 --- a/ThreatSource/src/Sensor/QuadrantDetector.cs +++ b/ThreatSource/src/Sensor/QuadrantDetector.cs @@ -224,13 +224,13 @@ namespace ThreatSource.Sensor // 简化模型:根据光斑中心到各象限中心的距离计算信号强度 // 象限中心坐标 double quadrantRadius = detectorRadius / 2; - Vector2D[] quadrantCenters = new Vector2D[4] - { + Vector2D[] quadrantCenters = + [ new Vector2D(quadrantRadius, quadrantRadius), // 右上 new Vector2D(-quadrantRadius, quadrantRadius), // 左上 new Vector2D(-quadrantRadius, -quadrantRadius), // 左下 new Vector2D(quadrantRadius, -quadrantRadius) // 右下 - }; + ]; // 计算每个象限接收到的信号强度 double totalWeight = 0; diff --git a/ThreatSource/src/Simulation/KinematicState.cs b/ThreatSource/src/Simulation/KinematicState.cs index b1f41f8..3d71ea3 100644 --- a/ThreatSource/src/Simulation/KinematicState.cs +++ b/ThreatSource/src/Simulation/KinematicState.cs @@ -50,7 +50,7 @@ namespace ThreatSource.Simulation set { _speed = value; - if (_velocity != null && _velocity.MagnitudeSquared() > 1e-9) // 容差,避免浮点问题 + if (_velocity.MagnitudeSquared() > 1e-9) // 容差,避免浮点问题 { // 保持现有方向,更新模长 _velocity = _velocity.Normalize() * _speed; @@ -81,7 +81,7 @@ namespace ThreatSource.Simulation get => _velocity; set { - _velocity = value ?? Vector3D.Zero; //确保 _velocity 不为 null + _velocity = value; _speed = _velocity.Magnitude(); } } @@ -148,7 +148,7 @@ namespace ThreatSource.Simulation { Position = position; Velocity = velocity; // Setter 会同时更新 Speed - if (velocity != null && velocity.MagnitudeSquared() > 1e-9) + if (velocity.MagnitudeSquared() > 1e-9) { Orientation = Orientation.FromVector(velocity.Normalize()); } diff --git a/ThreatSource/src/Utils/Common.cs b/ThreatSource/src/Utils/Common.cs index 35e704e..0b8e9dd 100644 --- a/ThreatSource/src/Utils/Common.cs +++ b/ThreatSource/src/Utils/Common.cs @@ -1,466 +1,5 @@ -using System; -using System.Numerics; - namespace ThreatSource.Utils { - /// - /// 表示三维空间中的向量,提供基本的向量运算功能 - /// - /// - /// 该类实现了三维向量的基本运算,包括: - /// - 向量的加减乘除运算 - /// - 向量的点积和叉积 - /// - 向量的归一化 - /// - 向量的旋转变换 - /// 所有计算都使用双精度浮点数(double)以保证精度 - /// - public class Vector3D - { - /// - /// X轴单位向量 (1, 0, 0) - /// - public static Vector3D UnitX => new Vector3D(1, 0, 0); - - /// - /// Y轴单位向量 (0, 1, 0) - /// - public static Vector3D UnitY => new Vector3D(0, 1, 0); - - /// - /// Z轴单位向量 (0, 0, 1) - /// - public static Vector3D UnitZ => new Vector3D(0, 0, 1); - - /// - /// 零向量 (0, 0, 0) - /// - public static Vector3D Zero => new Vector3D(0, 0, 0); - - /// - /// 用于浮点数比较的小量阈值 - /// - private const double EPSILON = 1e-10; - - /// - /// 获取或设置向量的X分量 - /// - public double X { get; set; } - - /// - /// 获取或设置向量的Y分量 - /// - public double Y { get; set; } - - /// - /// 获取或设置向量的Z分量 - /// - public double Z { get; set; } - - /// - /// 初始化三维向量的新实例 - /// - /// X分量的值 - /// Y分量的值 - /// Z分量的值 - public Vector3D(double x, double y, double z) - { - X = x; - Y = y; - Z = z; - } - - /// - /// 计算两个向量之间的距离 - /// - /// 第一个向量 - /// 第二个向量 - /// 两个向量之间的距离 - public static double Distance(Vector3D v1, Vector3D v2) - { - return Math.Sqrt(Math.Pow(v1.X - v2.X, 2) + Math.Pow(v1.Y - v2.Y, 2) + Math.Pow(v1.Z - v2.Z, 2)); - } - - - /// - /// 将向量转换为字符串表示 - /// - /// 向量的字符串表示 - public override string ToString() - { - return $"({X:F2}, {Y:F2}, {Z:F2})"; - } - - /// - /// 向量减法运算符重载 - /// - public static Vector3D operator -(Vector3D a, Vector3D b) - { - return new Vector3D(a.X - b.X, a.Y - b.Y, a.Z - b.Z); - } - - /// - /// 向量加法运算符重载 - /// - public static Vector3D operator +(Vector3D a, Vector3D b) - { - return new Vector3D(a.X + b.X, a.Y + b.Y, a.Z + b.Z); - } - - /// - /// 向量与标量乘法运算符重载 - /// - public static Vector3D operator *(Vector3D a, double scalar) - { - return new Vector3D(a.X * scalar, a.Y * scalar, a.Z * scalar); - } - - /// - /// 向量与标量除法运算符重载 - /// - public static Vector3D operator /(Vector3D a, double scalar) - { - return new Vector3D(a.X / scalar, a.Y / scalar, a.Z / scalar); - } - - /// - /// 向量反向 - /// - public static Vector3D operator -(Vector3D a) - { - return new Vector3D(-a.X, -a.Y, -a.Z); - } - - /// - /// 向量相等运算符重载 - /// - public static bool operator ==(Vector3D? left, Vector3D? right) - { - if (ReferenceEquals(left, right)) return true; - if (left is null || right is null) return false; - return left.X == right.X && left.Y == right.Y && left.Z == right.Z; - } - - /// - /// 向量不相等运算符重载 - /// - public static bool operator !=(Vector3D? left, Vector3D? right) - { - return !(left == right); - } - - /// - /// 判断两个向量是否相等 - /// - public override bool Equals(object? obj) - { - if (obj is Vector3D other) - { - return this == other; - } - return false; - } - - /// - /// 获取向量的哈希码 - /// - /// 向量的哈希码 - public override int GetHashCode() - { - return HashCode.Combine(X, Y, Z); - } - - /// - /// 计算向量的模长 - /// - /// 向量的模长 - public double Magnitude() - { - return Math.Sqrt(X * X + Y * Y + Z * Z); - } - - /// - /// 计算向量模长的平方 - /// - /// 向量模长的平方 - public double MagnitudeSquared() - { - return X * X + Y * Y + Z * Z; - } - - /// - /// 向量归一化 - /// - /// 归一化后的向量 - /// - /// 如果向量的模长小于 EPSILON,则返回零向量 - /// 否则返回单位向量 - /// - public Vector3D Normalize() - { - double mag = Magnitude(); - if (mag > EPSILON) - { - return new Vector3D(X / mag, Y / mag, Z / mag); - } - return Vector3D.Zero; - } - - /// - /// 检查向量是否为单位向量 - /// - /// 允许的误差范围,默认为 EPSILON - /// 如果向量是单位向量则返回 true,否则返回 false - /// - /// 通过检查向量的模长平方是否在 1.0 的 tolerance 范围内来判断 - /// - public bool IsUnit(double tolerance = EPSILON) - { - return Math.Abs(MagnitudeSquared() - 1.0) < tolerance; - } - - /// - /// 计算两个向量的叉积 - /// - /// 第一个向量 - /// 第二个向量 - /// 叉积结果 - public static Vector3D CrossProduct(Vector3D a, Vector3D b) - { - return new Vector3D( - a.Y * b.Z - a.Z * b.Y, - a.Z * b.X - a.X * b.Z, - a.X * b.Y - a.Y * b.X - ); - } - - /// - /// 计算两个向量的点积 - /// - /// 第一个向量 - /// 第二个向量 - /// 点积结果 - public static double DotProduct(Vector3D a, Vector3D b) - { - return a.X * b.X + a.Y * b.Y + a.Z * b.Z; - } - - /// - /// 向量取反 - /// - /// 输入向量 - /// 取反后的向量 - public static Vector3D Negate(Vector3D a) - { - return new Vector3D(-a.X, -a.Y, -a.Z); - } - - /// - /// 计算AB两点之间距离 A 点一定距离的点的坐标 - /// - /// 直线起点 - /// 直线终点 - /// 距离起点距离 - /// 直线上的点 - public static Vector3D PointOnLine(Vector3D a, Vector3D b, double distance) - { - Vector3D direction = b - a; - Vector3D unitDirection = direction.Normalize(); - return a + unitDirection * distance; - } - - internal Vector3D Rotate(Orientation orientation, double misalignmentAngle) - { - // 首先,根据方向和失调角计算出旋转矩阵 - Matrix4x4 rotationMatrix = CalculateRotationMatrix(orientation, misalignmentAngle); - - // 然后,使用旋转矩阵对当前向量进行旋转 - Vector3D rotatedVector = ApplyRotationMatrix(this, rotationMatrix); - return rotatedVector; - } - - /// - /// 计算旋转矩阵 - /// - /// 方向 - /// 失调角 - /// 旋转矩阵 - public static Matrix4x4 CalculateRotationMatrix(Orientation orientation, double misalignmentAngle) - { - // 创建旋转矩阵 - Matrix4x4 yawRotation = Matrix4x4.CreateRotationY((float)orientation.Yaw); - Matrix4x4 pitchRotation = Matrix4x4.CreateRotationX((float)orientation.Pitch); - Matrix4x4 rollRotation = Matrix4x4.CreateRotationZ((float)orientation.Roll); - Matrix4x4 misalignmentRotation = Matrix4x4.CreateRotationY((float)misalignmentAngle); - - // 组合旋转矩阵 - return misalignmentRotation * yawRotation * pitchRotation * rollRotation; - } - - private static Vector3D ApplyRotationMatrix(Vector3D vector, Matrix4x4 matrix) - { - double x = vector.X * matrix.M11 + vector.Y * matrix.M21 + vector.Z * matrix.M31 + matrix.M41; - double y = vector.X * matrix.M12 + vector.Y * matrix.M22 + vector.Z * matrix.M32 + matrix.M42; - double z = vector.X * matrix.M13 + vector.Y * matrix.M23 + vector.Z * matrix.M33 + matrix.M43; - return new Vector3D(x, y, z); - } - - /// - /// 使用矩阵变换方向向量(不应用平移) - /// - /// 要变换的方向向量 - /// 变换矩阵 - /// 变换后的方向向量 - public static Vector3D TransformDirection(Vector3D direction, Matrix4x4 matrix) - { - double x = direction.X * matrix.M11 + direction.Y * matrix.M21 + direction.Z * matrix.M31; - double y = direction.X * matrix.M12 + direction.Y * matrix.M22 + direction.Z * matrix.M32; - double z = direction.X * matrix.M13 + direction.Y * matrix.M23 + direction.Z * matrix.M33; - return new Vector3D(x, y, z); - } - - /// - /// 计算两个向量之间的角度 - /// - /// 第一个向量 - /// 第二个向量 - /// 两个向量之间的角度(弧度) - /// - /// 该方法会先对向量进行归一化,然后计算它们之间的夹角。 - /// 结果总是在 [0, π] 范围内。 - /// - public static double AngleBetween(Vector3D a, Vector3D b) - { - // 先归一化 - Vector3D normalizedA = a.Normalize(); - Vector3D normalizedB = b.Normalize(); - - // 计算点积并限制在 [-1, 1] 范围内 - double dot = DotProduct(normalizedA, normalizedB); - dot = Math.Max(-1.0, Math.Min(1.0, dot)); - - return Math.Acos(dot); - } - } - - /// - /// 表示三维空间中的方向,使用欧拉角表示 - /// - /// - /// 使用欧拉角(偏航角、俯仰角、滚转角)来表示三维空间中的方向: - /// - 偏航角(Yaw):绕Y轴旋转的角度 - /// - 俯仰角(Pitch):绕X轴旋转的角度 - /// - 滚转角(Roll):绕Z轴旋转的角度 - /// 所有角度均使用弧度制 - /// - public struct Orientation - { - /// - /// 获取或设置偏航角(绕Y轴旋转的角度),单位:弧度 - /// - public double Yaw { get; set; } - - /// - /// 获取或设置俯仰角(绕X轴旋转的角度),单位:弧度 - /// - public double Pitch { get; set; } - - /// - /// 获取或设置滚转角(绕Z轴旋转的角度),单位:弧度 - /// - public double Roll { get; set; } - - /// - /// 初始化方向的新实例 - /// - /// 偏航角,单位:弧度 - /// 俯仰角,单位:弧度 - /// 滚转角,单位:弧度 - public Orientation(double yaw, double pitch, double roll) - { - Yaw = yaw; - Pitch = pitch; - Roll = roll; - } - - /// - /// 将方向转换为字符串表示 - /// - /// 方向的字符串表示 - public override readonly string ToString() - { - return $"(Yaw: {Yaw:F2}, Pitch: {Pitch:F2}, Roll: {Roll:F2})"; - } - - /// - /// 将角度归一化到 [-π, π] 范围内 - /// - public void Normalize() - { - Yaw = NormalizeAngle(Yaw); - Pitch = NormalizeAngle(Pitch); - Roll = NormalizeAngle(Roll); - } - - /// - /// 将单个角度归一化到 [-π, π] 范围内 - /// - /// 输入角度 - /// 归一化后的角度 - private static double NormalizeAngle(double angle) - { - while (angle > Math.PI) angle -= 2 * Math.PI; - while (angle <= -Math.PI) angle += 2 * Math.PI; - return angle; - } - - internal Vector3D Rotate(Vector3D vector, double misalignmentAngle) - { - throw new NotImplementedException(); - } - - /// - /// 将方向转换为单位向量 - /// - /// 对应的单位向量 - public readonly Vector3D ToVector() - { - // Yaw: 绕 Y 轴 (Up), Pitch: 绕 X 轴 (Right) - // -Z Forward convention - // X_forward = -cos(Pitch) * sin(Yaw) - // Y_forward = sin(Pitch) - // Z_forward = -cos(Pitch) * cos(Yaw) - double cosPitch = Math.Cos(Pitch); - double sinPitch = Math.Sin(Pitch); - double cosYaw = Math.Cos(Yaw); - double sinYaw = Math.Sin(Yaw); - - double x = -cosPitch * sinYaw; - double y = sinPitch; - double z = -cosPitch * cosYaw; - return new Vector3D(x, y, z); - } - - /// - /// 从向量创建方向 (基于向量代表期望的 -Z 前向) - /// - /// 输入的世界前向单位向量 - /// 对应的方向 (RHS/CCW 欧拉角) - public static Orientation FromVector(Vector3D worldForwardVector) - { - Vector3D fwd = worldForwardVector.Normalize(); - // Pitch = arcsin(fwd.Y) - // Yaw = atan2(-fwd.X, -fwd.Z) - double pitch = Math.Asin(fwd.Y); // Pitch 范围 [-PI/2, PI/2] - double yaw = Math.Atan2(-fwd.X, -fwd.Z); // Yaw 范围 [-PI, PI] - - // Gimbal lock: 当 pitch 为 +/-PI/2 时,fwd.X 和 fwd.Z 理论上为0。 - // Math.Atan2(0,0) 通常返回0,这对于偏航角来说是一个合理的约定。 - // 此时,滚转角 Roll 和偏航角 Yaw 会指向同一旋转自由度。我们约定 Roll 为0。 - - return new Orientation(yaw, pitch, 0); // Roll 设为 0 - } - } - /// /// 表示二维平面上的向量 /// @@ -493,7 +32,7 @@ namespace ThreatSource.Utils /// /// 零向量 (0, 0) /// - public static Vector2D Zero => new Vector2D(0, 0); + public static Vector2D Zero => new(0, 0); } /// diff --git a/ThreatSource/src/Utils/MotionAlgorithm.cs b/ThreatSource/src/Utils/MotionAlgorithm.cs index 0bd575b..0176e7b 100644 --- a/ThreatSource/src/Utils/MotionAlgorithm.cs +++ b/ThreatSource/src/Utils/MotionAlgorithm.cs @@ -394,38 +394,21 @@ namespace ThreatSource.Utils Vector3D? initialGuessVelocityForDrag = null ) { - Vector3D effectiveInitialVelocityForDragEstimation; - if (initialGuessVelocityForDrag != null && initialGuessVelocityForDrag.MagnitudeSquared() > 1e-9) - { - effectiveInitialVelocityForDragEstimation = initialGuessVelocityForDrag; - } - else - { - Vector3D directionToTarget = targetPos - startPos; - if (directionToTarget.MagnitudeSquared() < 1e-9) - { - // 起点终点过近,或 initialSpeed 为零,难以确定有意义的拖拽估算方向 - // 尝试使用垂直向上的速度估算,如果 initialSpeed 也为零,则阻力为零 - if (initialSpeed < 1e-6) - { - effectiveInitialVelocityForDragEstimation = Vector3D.Zero; - } - else - { - effectiveInitialVelocityForDragEstimation = new Vector3D(0, initialSpeed, 0); - } - } - else - { - effectiveInitialVelocityForDragEstimation = directionToTarget.Normalize() * initialSpeed; - } - } + Vector3D targetDirection = (targetPos - startPos).Normalize(); + Vector3D currentVelocity = targetDirection * initialSpeed; - Vector3D constantDragAccelerationVector = CalculateDragAcceleration(effectiveInitialVelocityForDragEstimation, windVector, projectileMass); + // 如果提供了用于计算初始阻力的速度估算,则使用它;否则使用基于目标方向和初速的估算 + Vector3D velocityForDragEstimation = initialGuessVelocityForDrag.HasValue && initialGuessVelocityForDrag.Value.MagnitudeSquared() > 1e-9 + ? initialGuessVelocityForDrag.Value + : currentVelocity; + Vector3D dragAcceleration = CalculateDragAcceleration(velocityForDragEstimation, windVector, projectileMass); + + // 使用阻力调整后的初始速度计算发射角度 + // (这是一个简化模型,实际情况更复杂,可能需要迭代求解) Vector3D deltaPos = targetPos - startPos; Vector3D gravityVector = new(0, -Gravity, 0); - Vector3D effectiveTotalAcceleration = gravityVector + constantDragAccelerationVector; + Vector3D effectiveTotalAcceleration = gravityVector + dragAcceleration; double coeff_a = 0.25 * effectiveTotalAcceleration.MagnitudeSquared(); double coeff_b = -(Vector3D.DotProduct(deltaPos, effectiveTotalAcceleration) + initialSpeed * initialSpeed); diff --git a/ThreatSource/src/Utils/Orientation.cs b/ThreatSource/src/Utils/Orientation.cs new file mode 100644 index 0000000..6d6b34e --- /dev/null +++ b/ThreatSource/src/Utils/Orientation.cs @@ -0,0 +1,235 @@ +using System; // Mathのため、System名前空間が必要 + +namespace ThreatSource.Utils +{ + /// + /// 表示一个3x3的旋转矩阵。 + /// + public readonly struct Matrix3x3 + { + /// M11 + public readonly double M11; + /// M12 + public readonly double M12; + /// M13 + public readonly double M13; + /// M21 + public readonly double M21; + /// M22 + public readonly double M22; + /// M23 + public readonly double M23; + /// M31 + public readonly double M31; + /// M32 + public readonly double M32; + /// M33 + public readonly double M33; + + /// + /// 初始化 结构体的新实例。 + /// + public Matrix3x3(double m11, double m12, double m13, + double m21, double m22, double m23, + double m31, double m32, double m33) + { + M11 = m11; M12 = m12; M13 = m13; + M21 = m21; M22 = m22; M23 = m23; + M31 = m31; M32 = m32; M33 = m33; + } + + /// + /// 获取单位矩阵。 + /// + public static Matrix3x3 Identity { get; } = new Matrix3x3( + 1, 0, 0, + 0, 1, 0, + 0, 0, 1 + ); + + /// + /// 通过此矩阵变换指定的向量。 + /// + /// 要变换的向量。 + /// 变换后的向量。 + public readonly Vector3D Transform(Vector3D vector) + { + return new Vector3D( + M11 * vector.X + M12 * vector.Y + M13 * vector.Z, + M21 * vector.X + M22 * vector.Y + M23 * vector.Z, + M31 * vector.X + M32 * vector.Y + M33 * vector.Z + ); + } + + /// + /// 计算并返回此矩阵的转置。 + /// + /// 转置后的矩阵。 + /// 对于旋转矩阵,转置矩阵即为其逆矩阵。 + public readonly Matrix3x3 Transpose() + { + return new Matrix3x3( + M11, M21, M31, + M12, M22, M32, + M13, M23, M33 + ); + } + } + + /// + /// 表示三维空间中的方向,使用欧拉角表示 + /// + /// + /// 使用欧拉角(偏航角、俯仰角、滚转角)来表示三维空间中的方向: + /// - 偏航角(Yaw):绕Y轴旋转的角度 + /// - 俯仰角(Pitch):绕X轴旋转的角度 + /// - 滚转角(Roll):绕Z轴旋转的角度 + /// 所有角度均使用弧度制 + /// + public struct Orientation + { + /// + /// 获取或设置偏航角(绕Y轴旋转的角度),单位:弧度 + /// + public double Yaw { get; set; } + + /// + /// 获取或设置俯仰角(绕X轴旋转的角度),单位:弧度 + /// + public double Pitch { get; set; } + + /// + /// 获取或设置滚转角(绕Z轴旋转的角度),单位:弧度 + /// + public double Roll { get; set; } + + /// + /// 初始化方向的新实例 + /// + /// 偏航角,单位:弧度 + /// 俯仰角,单位:弧度 + /// 滚转角,单位:弧度 + public Orientation(double yaw, double pitch, double roll) + { + Yaw = yaw; + Pitch = pitch; + Roll = roll; + } + + /// + /// 将方向转换为字符串表示 + /// + /// 方向的字符串表示 + public override readonly string ToString() + { + return $"(Yaw: {Yaw:F2}, Pitch: {Pitch:F2}, Roll: {Roll:F2})"; + } + + /// + /// 将角度归一化到 [-π, π] 范围内 + /// + public void Normalize() + { + Yaw = NormalizeAngle(Yaw); + Pitch = NormalizeAngle(Pitch); + Roll = NormalizeAngle(Roll); + } + + /// + /// 将单个角度归一化到 [-π, π] 范围内 + /// + /// 输入角度 + /// 归一化后的角度 + private static double NormalizeAngle(double angle) + { + while (angle > Math.PI) angle -= 2 * Math.PI; + while (angle <= -Math.PI) angle += 2 * Math.PI; + return angle; + } + + internal Vector3D Rotate(Vector3D vector, double misalignmentAngle) + { + throw new NotImplementedException(); + } + + /// + /// 将方向转换为表示从局部坐标系到父坐标系的旋转矩阵。 + /// 旋转顺序为 Rz(Roll) -> Rx(Pitch) -> Ry(Yaw)。 + /// 与ToVector() (-Z向前)的约定兼容。 + /// + /// 3x3的旋转矩阵。 + public readonly Matrix3x3 ToRotationMatrix() + { + double cY = Math.Cos(Yaw); + double sY = Math.Sin(Yaw); + double cP = Math.Cos(Pitch); + double sP = Math.Sin(Pitch); + double cR = Math.Cos(Roll); + double sR = Math.Sin(Roll); + + // Ry(Yaw) * Rx(Pitch) * Rz(Roll) + // m11 = cY*cR + sY*sP*sR + // m12 = -cY*sR + sY*sP*cR + // m13 = sY*cP + // m21 = cP*sR + // m22 = cP*cR + // m23 = -sP + // m31 = -sY*cR + cY*sP*sR + // m32 = sY*sR + cY*sP*cR + // m33 = cY*cP + + return new Matrix3x3( + cY * cR + sY * sP * sR, -cY * sR + sY * sP * cR, sY * cP, + cP * sR, cP * cR, -sP, + -sY * cR + cY * sP * sR, sY * sR + cY * sP * cR, cY * cP + ); + } + + /// + /// 将方向转换为表示从父坐标系到局部坐标系的旋转矩阵。 + /// 这是 ToRotationMatrix() 的逆变换。 + /// + /// 3x3的旋转矩阵 (世界到局部)。 + public readonly Matrix3x3 ToWorldToLocalRotationMatrix() + { + return ToRotationMatrix().Transpose(); + } + + /// + /// 将方向转换为单位向量 + /// + /// 对应的单位向量 + public readonly Vector3D ToVector() + { + // Yaw: 绕 Y 轴 (Up), Pitch: 绕 X 轴 (Right) + // -Z Forward convention + double cosPitch = Math.Cos(Pitch); + double sinPitch = Math.Sin(Pitch); + double cosYaw = Math.Cos(Yaw); + double sinYaw = Math.Sin(Yaw); + + double x = -cosPitch * sinYaw; + double y = sinPitch; + double z = -cosPitch * cosYaw; + return new Vector3D(x, y, z); + } + + /// + /// 从向量创建方向 (基于向量代表期望的 -Z 前向) + /// + /// 输入的世界前向单位向量 + /// 对应的方向 (RHS/CCW 欧拉角) + public static Orientation FromVector(Vector3D worldForwardVector) + { + Vector3D fwd = worldForwardVector.Normalize(); + double pitch = Math.Asin(fwd.Y); // Pitch 范围 [-PI/2, PI/2] + double yaw = Math.Atan2(-fwd.X, -fwd.Z); // Yaw 范围 [-PI, PI] + + // Gimbal lock: 当 pitch 为 +/-PI/2 时,fwd.X 和 fwd.Z 理论上为0。 + // Math.Atan2(0,0) 通常返回0,这对于偏航角来说是一个合理的约定。 + // 此时,滚转角 Roll 和偏航角 Yaw 会指向同一旋转自由度。我们约定 Roll 为0。 + + return new Orientation(yaw, pitch, 0); // Roll 设为 0 + } + } +} \ No newline at end of file diff --git a/ThreatSource/src/Utils/Vector3D.cs b/ThreatSource/src/Utils/Vector3D.cs new file mode 100644 index 0000000..faba580 --- /dev/null +++ b/ThreatSource/src/Utils/Vector3D.cs @@ -0,0 +1,343 @@ +using System.Numerics; + +namespace ThreatSource.Utils +{ + /// + /// 表示三维空间中的向量,提供基本的向量运算功能 + /// + /// + /// 该类实现了三维向量的基本运算,包括: + /// - 向量的加减乘除运算 + /// - 向量的点积和叉积 + /// - 向量的归一化 + /// - 向量的旋转变换 + /// 所有计算都使用双精度浮点数(double)以保证精度 + /// + public struct Vector3D + { + /// + /// X轴单位向量 (1, 0, 0) + /// + public static Vector3D UnitX => new(1, 0, 0); + + /// + /// Y轴单位向量 (0, 1, 0) + /// + public static Vector3D UnitY => new(0, 1, 0); + + /// + /// Z轴单位向量 (0, 0, 1) + /// + public static Vector3D UnitZ => new(0, 0, 1); + + /// + /// 零向量 (0, 0, 0) + /// + public static Vector3D Zero => new(0, 0, 0); + + /// + /// 用于浮点数比较的小量阈值 + /// + private const double EPSILON = 1e-10; + + /// + /// 获取或设置向量的X分量 + /// + public double X { get; set; } + + /// + /// 获取或设置向量的Y分量 + /// + public double Y { get; set; } + + /// + /// 获取或设置向量的Z分量 + /// + public double Z { get; set; } + + /// + /// 初始化三维向量的新实例 + /// + /// X分量的值 + /// Y分量的值 + /// Z分量的值 + public Vector3D(double x, double y, double z) + { + X = x; + Y = y; + Z = z; + } + + /// + /// 计算两个向量之间的距离 + /// + /// 第一个向量 + /// 第二个向量 + /// 两个向量之间的距离 + public static double Distance(Vector3D v1, Vector3D v2) + { + double deltaX = v1.X - v2.X; + double deltaY = v1.Y - v2.Y; + double deltaZ = v1.Z - v2.Z; + return Math.Sqrt(deltaX * deltaX + deltaY * deltaY + deltaZ * deltaZ); + } + + /// + /// 将向量转换为字符串表示 + /// + /// 向量的字符串表示 + public override readonly string ToString() + { + return $"({X:F2}, {Y:F2}, {Z:F2})"; + } + + /// + /// 向量减法运算符重载 + /// + public static Vector3D operator -(Vector3D a, Vector3D b) + { + return new Vector3D(a.X - b.X, a.Y - b.Y, a.Z - b.Z); + } + + /// + /// 向量加法运算符重载 + /// + public static Vector3D operator +(Vector3D a, Vector3D b) + { + return new Vector3D(a.X + b.X, a.Y + b.Y, a.Z + b.Z); + } + + /// + /// 向量与标量乘法运算符重载 + /// + public static Vector3D operator *(Vector3D a, double scalar) + { + return new Vector3D(a.X * scalar, a.Y * scalar, a.Z * scalar); + } + + /// + /// 向量与标量除法运算符重载 + /// + public static Vector3D operator /(Vector3D a, double scalar) + { + return new Vector3D(a.X / scalar, a.Y / scalar, a.Z / scalar); + } + + /// + /// 向量反向 + /// + public static Vector3D operator -(Vector3D a) + { + return new Vector3D(-a.X, -a.Y, -a.Z); + } + + /// + /// 向量相等运算符重载 + /// + public static bool operator ==(Vector3D left, Vector3D right) + { + return left.X == right.X && left.Y == right.Y && left.Z == right.Z; + } + + /// + /// 向量不相等运算符重载 + /// + public static bool operator !=(Vector3D left, Vector3D right) + { + return !(left == right); + } + + /// + /// 判断两个向量是否相等 + /// + public override readonly bool Equals(object? obj) + { + if (obj is Vector3D other) + { + return this == other; + } + return false; + } + + /// + /// 获取向量的哈希码 + /// + /// 向量的哈希码 + public override readonly int GetHashCode() + { + return HashCode.Combine(X, Y, Z); + } + + /// + /// 计算向量的模长 + /// + /// 向量的模长 + public readonly double Magnitude() + { + return Math.Sqrt(X * X + Y * Y + Z * Z); + } + + /// + /// 计算向量模长的平方 + /// + /// 向量模长的平方 + public readonly double MagnitudeSquared() + { + return X * X + Y * Y + Z * Z; + } + + /// + /// 向量归一化 + /// + /// 归一化后的向量 + /// + /// 如果向量的模长小于 EPSILON,则返回零向量 + /// 否则返回单位向量 + /// + public readonly Vector3D Normalize() + { + double mag = Magnitude(); + if (mag > EPSILON) + { + return new Vector3D(X / mag, Y / mag, Z / mag); + } + return Zero; + } + + /// + /// 检查向量是否为单位向量 + /// + /// 允许的误差范围,默认为 EPSILON + /// 如果向量是单位向量则返回 true,否则返回 false + /// + /// 通过检查向量的模长平方是否在 1.0 的 tolerance 范围内来判断 + /// + public readonly bool IsUnit(double tolerance = EPSILON) + { + return Math.Abs(MagnitudeSquared() - 1.0) < tolerance; + } + + /// + /// 计算两个向量的叉积 + /// + /// 第一个向量 + /// 第二个向量 + /// 叉积结果 + public static Vector3D CrossProduct(Vector3D a, Vector3D b) + { + return new Vector3D( + a.Y * b.Z - a.Z * b.Y, + a.Z * b.X - a.X * b.Z, + a.X * b.Y - a.Y * b.X + ); + } + + /// + /// 计算两个向量的点积 + /// + /// 第一个向量 + /// 第二个向量 + /// 点积结果 + public static double DotProduct(Vector3D a, Vector3D b) + { + return a.X * b.X + a.Y * b.Y + a.Z * b.Z; + } + + /// + /// 向量取反 + /// + /// 输入向量 + /// 取反后的向量 + public static Vector3D Negate(Vector3D a) + { + return new Vector3D(-a.X, -a.Y, -a.Z); + } + + /// + /// 计算AB两点之间距离 A 点一定距离的点的坐标 + /// + /// 直线起点 + /// 直线终点 + /// 距离起点距离 + /// 直线上的点 + public static Vector3D PointOnLine(Vector3D a, Vector3D b, double distance) + { + Vector3D direction = b - a; + Vector3D unitDirection = direction.Normalize(); + return a + unitDirection * distance; + } + + internal readonly Vector3D Rotate(Orientation orientation, double misalignmentAngle) + { + // 首先,根据方向和失调角计算出旋转矩阵 + Matrix4x4 rotationMatrix = CalculateRotationMatrix(orientation, misalignmentAngle); + + // 然后,使用旋转矩阵对当前向量进行旋转 + Vector3D rotatedVector = ApplyRotationMatrix(this, rotationMatrix); + return rotatedVector; + } + + /// + /// 计算旋转矩阵 + /// + /// 方向 + /// 失调角 + /// 旋转矩阵 + public static Matrix4x4 CalculateRotationMatrix(Orientation orientation, double misalignmentAngle) + { + // 创建旋转矩阵 + Matrix4x4 yawRotation = Matrix4x4.CreateRotationY((float)orientation.Yaw); + Matrix4x4 pitchRotation = Matrix4x4.CreateRotationX((float)orientation.Pitch); + Matrix4x4 rollRotation = Matrix4x4.CreateRotationZ((float)orientation.Roll); + Matrix4x4 misalignmentRotation = Matrix4x4.CreateRotationY((float)misalignmentAngle); + + // 组合旋转矩阵 + return misalignmentRotation * yawRotation * pitchRotation * rollRotation; + } + + private static Vector3D ApplyRotationMatrix(Vector3D vector, Matrix4x4 matrix) + { + double x = vector.X * matrix.M11 + vector.Y * matrix.M21 + vector.Z * matrix.M31 + matrix.M41; + double y = vector.X * matrix.M12 + vector.Y * matrix.M22 + vector.Z * matrix.M32 + matrix.M42; + double z = vector.X * matrix.M13 + vector.Y * matrix.M23 + vector.Z * matrix.M33 + matrix.M43; + return new Vector3D(x, y, z); + } + + /// + /// 使用矩阵变换方向向量(不应用平移) + /// + /// 要变换的方向向量 + /// 变换矩阵 + /// 变换后的方向向量 + public static Vector3D TransformDirection(Vector3D direction, Matrix4x4 matrix) + { + double x = direction.X * matrix.M11 + direction.Y * matrix.M21 + direction.Z * matrix.M31; + double y = direction.X * matrix.M12 + direction.Y * matrix.M22 + direction.Z * matrix.M32; + double z = direction.X * matrix.M13 + direction.Y * matrix.M23 + direction.Z * matrix.M33; + return new Vector3D(x, y, z); + } + + /// + /// 计算两个向量之间的角度 + /// + /// 第一个向量 + /// 第二个向量 + /// 两个向量之间的角度(弧度) + /// + /// 该方法会先对向量进行归一化,然后计算它们之间的夹角。 + /// 结果总是在 [0, π] 范围内。 + /// + public static double AngleBetween(Vector3D a, Vector3D b) + { + // 先归一化 + Vector3D normalizedA = a.Normalize(); + Vector3D normalizedB = b.Normalize(); + + // 计算点积并限制在 [-1, 1] 范围内 + double dot = DotProduct(normalizedA, normalizedB); + dot = Math.Max(-1.0, Math.Min(1.0, dot)); + + return Math.Acos(dot); + } + } +} \ No newline at end of file diff --git a/docs/project/rcs_model_design.md b/docs/project/rcs_model_design.md new file mode 100644 index 0000000..925eb69 --- /dev/null +++ b/docs/project/rcs_model_design.md @@ -0,0 +1,103 @@ +# RCS 模型设计 (`RcsPattern`) + +`RcsPattern` 类用于管理目标实体在不同观察角度下的雷达散射截面 (Radar Cross Section, RCS) 数据。它采用一个详细的、基于观察者局部视角的模型来确定RCS值。 + +## 核心数据结构 + +- **内部RCS矩阵**: 核心数据存储在一个私有的 `double[,] RcsMatrixData` (6行4列) 中,单位为dBsm (分贝平方米)。 +- **数据源**: RCS数据通常从TOML配置文件中加载。为此,类包含一个公共属性 `public List> RcsMatrixDataSource { get; set; }`。TOML解析器(如Tomlyn)会将配置文件中 `RcsMatrix.Data` 数组映射到此属性。`RcsMatrixData` 则通过按需转换 `RcsMatrixDataSource` 得到。 +- **默认值**: 如果数据源未提供或不完整,将使用预定义的默认RCS值 (`DEFAULT_RCS_DBSM`)。 + +## RCS矩阵定义 + +RCS矩阵按以下方式组织: + +### 1. 行定义 (主方向) + +矩阵的6行分别对应目标在观察者视角的六个主要朝向: + +- **行 0**: 前 (Front) - 观察者从目标正前方观察。 +- **行 1**: 后 (Back) - 观察者从目标正后方观察。 +- **行 2**: 左 (Left) - 观察者从目标正左方观察。 +- **行 3**: 右 (Right) - 观察者从目标正右方观察。 +- **行 4**: 上 (Top) - 观察者从目标正上方观察。 +- **行 5**: 下 (Bottom) - 观察者从目标正下方观察。 + +主方向是根据观察向量的全局俯仰角和方位角确定的。俯仰角大于70度为"上",小于-70度为"下"。其余根据全局方位角(相对于目标前进方向)划分为前、后、左、右各90度扇区。 + +### 2. 列定义 (象限) + +矩阵的4列代表每个主方向下的四个象限。这些象限是严格根据观察者相对于当前被观察主方向的**局部视角**来划分的。这意味着,无论观察哪个主方向,"左、右、上、下"象限的相对含义是一致的。 + +**统一象限定义** (从观察者视角看当前主平面): +- **列 0**: 观察者位于当前主方向局部参考系的"右侧"和"上半部分"。 +- **列 1**: 观察者位于当前主方向局部参考系的"左侧"和"上半部分"。 +- **列 2**: 观察者位于当前主方向局部参考系的"左侧"和"下半部分"。 +- **列 3**: 观察者位于当前主方向局部参考系的"右侧"和"下半部分"。 + +**局部参考轴的确定**: +- **对于侧面 (前、后、左、右)**: + - 局部"上/下"部分:由观察者方向在目标的全局上方向 (`targetUpVectorInTargetFrame`) 的投影决定(正为上,负为下)。 + - 局部"左/右"侧:由观察者方向在当前主方向的局部右轴 (`localRight`) 上的投影决定(正为右,负为左)。`localRight` 是根据当前主方向(前、后、左、右)定义的。 +- **对于顶面/底面**: + - 局部"上/下"部分 (这里更准确地理解为"前/后"部分):由观察者方向在目标的全局前进方向 (`targetForwardVectorInTargetFrame`) 的投影决定(正为前/上,负为后/下)。 + - 局部"左/右"侧:由观察者方向在目标的全局右方向 (`targetRightVectorInTargetFrame`,作为顶/底面的局部右轴) 上的投影决定(正为右,负为左)。 + +## RCS值查询方法 + +提供了两种方法来获取RCS值: + +1. **`GetRcsValueDbSm(Vector3D observerDirectionInTargetFrame, Vector3D targetForwardVectorInTargetFrame, Vector3D targetUpVectorInTargetFrame, Vector3D targetRightVectorInTargetFrame)`**: + * **推荐使用此版本**。 + * 它接收目标坐标系中的观察向量和目标自身姿态向量。 + * 通过计算观察者相对于目标各主方向局部参考轴的精确几何关系来确定RCS矩阵的行和列。 + * 提供最精确的、符合局部视角定义的RCS值。 + +2. **`GetRcsValueDbSm(double azimuthDeg, double elevationDeg)`**: + * 接收全局方位角和俯仰角。 + * 此方法使用简化的、基于全局角度的象限确定逻辑。虽然主方向(行)的确定与向量版本一致,但其象限(列)的划分可能与向量版本定义的精确局部视角不完全一致,特别是在处理目标后部等复杂情况时。 + * 可作为简化场景或无法提供完整向量信息时的备用。 + +## TOML 配置示例 + +以下是如何在TOML配置文件中定义6x4的RCS数据 (位于目标配置文件的 `Properties` 下,例如 `mbt_001.toml`): + +```toml +# ... (其他属性) ... + +# [Properties.RcsPattern] # Tomlyn 通常会自动处理嵌套结构 +# RcsMatrixDataSource = [ # 直接在 EquipmentProperties 中定义 RcsPattern 实例,并通过其 RcsMatrixDataSource 属性填充 +# ... +# ] +# 或者,如果 RcsPattern 是 EquipmentProperties 的一个直接可配置的子表: +[Properties.RcsPattern.RcsMatrixDataSource] # 更可能是这样,或直接是 RcsPattern = { RcsMatrixDataSource = [...] } 形式 +Data = [ + # 行0: 前 (相对于目标前进方向) + # 列 (观察者局部视角): 0(右上), 1(左上), 2(左下), 3(右下) + [-10.0, -11.0, -12.0, -13.0], # 前: [右前半球上方, 左前半球上方, 左前半球下方, 右前半球下方] + + # 行1: 后 (相对于目标后退方向) + # 列 (观察者局部视角): 0(右上), 1(左上), 2(左下), 3(右下) + [-10.5, -11.5, -12.5, -13.5], # 后: [右后半球上方, 左后半球上方, 左后半球下方, 右后半球下方] + + # 行2: 左 (相对于目标左侧方向) + # 列 (观察者局部视角): 0(右上), 1(左上), 2(左下), 3(右下) + [-15.0, -16.0, -17.0, -18.0], # 左: [目标左侧的右半(偏后)上方, 左半(偏前)上方, 左半(偏前)下方, 右半(偏后)下方] + # 更准确:观察者在目标左侧,其视角中的 右上/左上/左下/右下 象限 + + # 行3: 右 (相对于目标右侧方向) + # 列 (观察者局部视角): 0(右上), 1(左上), 2(左下), 3(右下) + [-15.5, -16.5, -17.5, -18.5], # 右: [目标右侧的 右上/左上/左下/右下 象限,从观察者视角看] + + # 行4: 上 (相对于目标顶面) + # 列 (观察者局部视角): 0(右上), 1(左上), 2(左下), 3(右下) + # 局部上/下 -> 目标全局 前/后 + # 局部左/右 -> 目标全局 左/右 + [ -5.0, -6.0, -7.0, -8.0], # 上: [目标顶部的全局右前方块, 全局左前方块, 全局左后方块, 全局右后方块] + + # 行5: 下 (相对于目标底面) + # 列 (观察者局部视角): 0(右上), 1(左上), 2(左下), 3(右下) + [-20.0, -21.0, -22.0, -23.0] # 下: [目标底部的全局右前方块, 全局左前方块, 全局左后方块, 全局右后方块] +] +``` +**注意**: TOML示例中象限的文字描述是为了帮助理解,实际填充数据时应严格对应代码中通过 `isLocallyTopQuadrant` 和 `isLocallyRightQuadrant` 计算出的逻辑。对于"上"和"下"主方向,其"局部上/下"象限实际对应目标的全局"前/后"。 \ No newline at end of file diff --git a/docs/project/test_method.md b/docs/project/test_method.md new file mode 100644 index 0000000..0ff7232 --- /dev/null +++ b/docs/project/test_method.md @@ -0,0 +1,49 @@ +# 测试方法 + +## 测试框架 +MSTest + +### 测试命令 + +1. 测试全部用例 +```bash +dotnet test +``` + +2. 测试指定用例 +```bash +dotnet test --filter "FullyQualifiedName=ThreatSource.Tests.Utils.Vector3DPerformanceTests.Baseline_Vector3D_Class_Performance" +``` + +3. 测试指定用例并输出详细日志 +```bash +dotnet test --filter "FullyQualifiedName=ThreatSource.Tests.Utils.Vector3DPerformanceTests.Baseline_Vector3D_Class_Performance" --logger "console;verbosity=detailed" | cat +``` + +### 测试报告 +时间:2025-05-16 +测试目的:测试 Vector3D 作为类和结构体的性能差异 +测试用例:ThreatSource.Tests.Utils.Vector3DPerformanceTests.Baseline_Vector3D_Class_Performance + +测试结果: + +1. 这是更改为 struct 之前的性能(作为类的 Vector3D): +Creation: 67 ms +Addition: 29 ms +Subtraction: 19 ms +Scalar Mult: 16 ms +Dot Product: 11 ms +Cross Product: 24 ms +Magnitude: 12 ms +Normalization: 40 ms + +2. 这是更改为 struct 之后 的性能: +Creation: 21 ms +Addition: 23 ms +Subtraction: 22 ms +Scalar Mult: 15 ms +Dot Product: 12 ms +Cross Product: 25 ms +Magnitude: 14 ms +Normalization: 51 ms + diff --git a/tools/ComprehensiveMissileSimulator.cs b/tools/ComprehensiveMissileSimulator.cs index 8cb6728..70258c1 100644 --- a/tools/ComprehensiveMissileSimulator.cs +++ b/tools/ComprehensiveMissileSimulator.cs @@ -3,15 +3,10 @@ using ThreatSource.Simulation; using ThreatSource.Utils; using ThreatSource.Jammable; using ThreatSource.Jammer; -using ThreatSource.Sensor; using ThreatSource.Equipment; -using ThreatSource.Guidance; using ThreatSource.Indicator; using ThreatSource.Data; -using AirTransmission; using System.Diagnostics; -using System.Collections.Generic; -using System.Linq; namespace ThreatSource.Tools.MissileSimulation { @@ -51,16 +46,16 @@ namespace ThreatSource.Tools.MissileSimulation { get { - switch (SelectedMissileId) + return SelectedMissileId switch { - case "LSGM_1": return "激光半主动制导导弹"; - case "LBRM_1": return "激光驾束制导导弹"; - case "TSM_1": return "末敏子弹药"; - case "ICGM_1": return "红外指令制导导弹"; - case "ITGM_1": return "红外成像末制导导弹"; - case "MMWG_1": return "毫米波末制导导弹"; - default: return "未知导弹"; - } + "LSGM_1" => "激光半主动制导导弹", + "LBRM_1" => "激光驾束制导导弹", + "TSM_1" => "末敏子弹药", + "ICGM_1" => "红外指令制导导弹", + "ITGM_1" => "红外成像末制导导弹", + "MMWG_1" => "毫米波末制导导弹", + _ => "未知导弹", + }; } } @@ -69,12 +64,12 @@ namespace ThreatSource.Tools.MissileSimulation /// public ComprehensiveMissileSimulator() { - // Initialize filter and listener first - this.logLevelFilter = new EventTypeFilter(SourceLevels.Verbose); // Default to Verbose - this.consoleListener = new ConsoleTraceListener(); - this.consoleListener.Filter = this.logLevelFilter; + // 首先初始化过滤器和监听器 + logLevelFilter = new EventTypeFilter(SourceLevels.Verbose); // 默认设置为Verbose + consoleListener = new ConsoleTraceListener(); + consoleListener.Filter = logLevelFilter; - // Clear any existing ConsoleTraceListeners and add our controlled instance + // 清除任何现有的ConsoleTraceListeners并添加我们的控制实例 for (int i = Trace.Listeners.Count - 1; i >= 0; i--) { if (Trace.Listeners[i] is ConsoleTraceListener) @@ -82,7 +77,7 @@ namespace ThreatSource.Tools.MissileSimulation Trace.Listeners.RemoveAt(i); } } - Trace.Listeners.Add(this.consoleListener); + Trace.Listeners.Add(consoleListener); simulationManager = new SimulationManager(); _dataManager = new ThreatSourceDataManager("../ThreatSource/data"); @@ -95,7 +90,7 @@ namespace ThreatSource.Tools.MissileSimulation missileJammingMap = []; SelectedMissileId = ""; - this.activeJammings = new List<(JammingType Type, string DisplayName, string JammerId, string Mode, string Target)>(); + activeJammings = []; // 初始化导弹-干扰映射 InitializeMissileJammingMap(); @@ -231,7 +226,7 @@ namespace ThreatSource.Tools.MissileSimulation Speed = 2.0 }; string targetId = "Tank_1"; - var target = _threatSourceFactory.CreateTarget(targetId, "mbt_001", motionParameters); + var target = _threatSourceFactory.CreateEquipment(targetId, "mbt_001", motionParameters); targets[targetId] = target; simulationManager.RegisterEntity(targetId, target); Console.WriteLine($"添加目标 {targetId},位置:{motionParameters.Position}"); @@ -732,9 +727,9 @@ namespace ThreatSource.Tools.MissileSimulation // Add to activeJammings list if not already present var jammingTuple = (Type: type, DisplayName: displayName, JammerId: jammerId, Mode: mode, Target: target); - if (!this.activeJammings.Any(j => j.JammerId == jammerId && j.Type == type)) // Simple check to avoid duplicates + if (!activeJammings.Any(j => j.JammerId == jammerId && j.Type == type)) // Simple check to avoid duplicates { - this.activeJammings.Add(jammingTuple); + activeJammings.Add(jammingTuple); } } else @@ -979,7 +974,7 @@ namespace ThreatSource.Tools.MissileSimulation Console.WriteLine($"关闭干扰器 {jammerId}: {displayName}"); // Remove from activeJammings list - this.activeJammings.RemoveAll(j => j.JammerId == jammerId && j.Type == type); + activeJammings.RemoveAll(j => j.JammerId == jammerId && j.Type == type); } } @@ -1047,12 +1042,12 @@ namespace ThreatSource.Tools.MissileSimulation PrintSimulationRegisteredComponents(); // 启动仿真系统 - simulationManager.StartSimulation(this.TimeStep); + simulationManager.StartSimulation(TimeStep); while (simulationManager.State == SimulationState.Running) { // 让仿真管理器统一处理所有实体的更新 - simulationManager.Update(this.TimeStep); + simulationManager.Update(TimeStep); // 检查是否还有活跃的导弹 var activeMissiles = simulationManager.GetEntitiesByType() @@ -1358,17 +1353,17 @@ namespace ThreatSource.Tools.MissileSimulation public string GetActiveJammingDescription() { - if (this.activeJammings == null || !this.activeJammings.Any()) + if (activeJammings == null || activeJammings.Count == 0) { return "(无干扰)"; } - return $"(已激活: {this.activeJammings.First().DisplayName})"; + return $"(已激活: {activeJammings.First().DisplayName})"; } public List<(JammingType Type, string DisplayName, string JammerId, string Mode, string Target)> GetCurrentlyActiveJammings() { - // Return a new list to prevent external modification of the internal list - return new List<(JammingType Type, string DisplayName, string JammerId, string Mode, string Target)>(this.activeJammings); + // 返回一个新的列表,防止外部修改内部列表 + return [.. activeJammings]; } } } \ No newline at end of file diff --git a/tools/Program.cs b/tools/Program.cs index 24ed3c4..cb38777 100644 --- a/tools/Program.cs +++ b/tools/Program.cs @@ -1,9 +1,4 @@ -using ThreatSource.Utils; -using ThreatSource.Jammer; -using System; -using System.Threading; using System.Diagnostics; -using System.Linq; namespace ThreatSource.Tools.MissileSimulation { @@ -13,7 +8,7 @@ namespace ThreatSource.Tools.MissileSimulation { Console.WriteLine("综合导弹模拟程序启动..."); var simulator = new ComprehensiveMissileSimulator(); - // Set a default log level on startup if desired, or rely on Simulator's default + // 如果需要,可以在启动时设置默认日志级别,或依赖Simulator的默认值 // simulator.SetLogLevel(SourceLevels.Information); ApplicationHostLoop(simulator); Console.WriteLine("\n程序已退出。"); @@ -32,13 +27,13 @@ namespace ThreatSource.Tools.MissileSimulation { var indicators = simulator.GetActiveIndicators(); var firstActiveIndicator = indicators.FirstOrDefault(ind => ind.IsActive); - if (firstActiveIndicator.Name != null) // Name is a string, check for null if tuple might not be found + if (firstActiveIndicator.Name != null) { sensorStatusString = $"(已激活: {firstActiveIndicator.Name})"; } else { - sensorStatusString = "(无默认激活指示器)"; // Or "(配置)" or similar + sensorStatusString = "(无默认激活指示器)"; } } else @@ -98,8 +93,6 @@ namespace ThreatSource.Tools.MissileSimulation } } } - - // Placeholder for ShowLogLevelMenu - to be implemented next static void ShowLogLevelMenu(ComprehensiveMissileSimulator simulator) { bool backToMainMenu = false; @@ -115,8 +108,8 @@ namespace ThreatSource.Tools.MissileSimulation Console.Write("请选择日志级别: "); string input = Console.ReadLine()?.ToLower() ?? string.Empty; - SourceLevels currentLevel = simulator.CurrentLogLevel; // Use the new public property - SourceLevels selectedLevel = currentLevel; // Default to current if invalid choice + SourceLevels currentLevel = simulator.CurrentLogLevel; + SourceLevels selectedLevel = currentLevel; // 默认使用当前级别 bool levelSet = false; switch (input) @@ -133,23 +126,22 @@ namespace ThreatSource.Tools.MissileSimulation if (levelSet) { simulator.SetLogLevel(selectedLevel); - // Confirmation is printed by SetLogLevel in simulator - return; // Return to main menu after setting level + // 设置日志级别后,会由simulator打印确认 + return; // 设置后返回主菜单 } } } - // Placeholder for RunSimulationInstance - to be implemented next static void RunSimulationInstance(ComprehensiveMissileSimulator simulator) { Console.WriteLine("\n--- 开始仿真 --- "); var simulationEndedEvent = new ManualResetEvent(false); simulator.SimulationEnded += (sender, e) => { simulationEndedEvent.Set(); - // Unsubscribe to prevent issues if RunSimulationInstance is called again - if (sender is ComprehensiveMissileSimulator sim) // C# 7.0 pattern matching + // 防止多次调用RunSimulationInstance时出现的问题 + if (sender is ComprehensiveMissileSimulator sim) { - sim.SimulationEnded -= (s, args) => simulationEndedEvent.Set(); // Attempt to unsubscribe, exact lambda match is tricky + sim.SimulationEnded -= (s, args) => simulationEndedEvent.Set(); // 尝试取消订阅,精确的lambda匹配比较困难 } }; @@ -160,28 +152,22 @@ namespace ThreatSource.Tools.MissileSimulation Console.WriteLine("仿真过程中,您可以随时按 Enter 键尝试提前中止当前仿真实例并返回主菜单。"); bool simulationForceStopped = false; - while (!simulationEndedEvent.WaitOne(100)) // Check every 100ms if simulation ended naturally + while (!simulationEndedEvent.WaitOne(100)) // 每100ms检查一次仿真是否结束 { if (Console.KeyAvailable) { - // Read the key, but don't wait if it was just a check. - // We want to allow Enter to break, not necessarily other typed commands during active sim. - // A simple Console.ReadLine() here would block until Enter. - // If user presses Enter, ReadLine will return empty or the character if it was typed fast enough. - var keyInfo = Console.ReadKey(intercept: true); // Intercept to prevent display + var keyInfo = Console.ReadKey(intercept: true); if (keyInfo.Key == ConsoleKey.Enter) { Console.WriteLine("\n用户请求中止当前仿真实例..."); - simulator.Stop(); // Request simulator to stop + simulator.Stop(); simulationForceStopped = true; - break; // Exit the wait loop + break; } - // If other keys are pressed, we can choose to ignore them or buffer them for a potential command parser later. - // For now, only Enter is an active interrupt. } } - // Wait for the simulation thread to actually finish, especially if force stopped. + // 等待仿真线程实际完成,特别是如果被强制停止。 simulationThread.Join(); if (simulationForceStopped) @@ -194,7 +180,7 @@ namespace ThreatSource.Tools.MissileSimulation } Console.WriteLine("按任意键返回主菜单..."); - Console.ReadKey(); // Wait for user to acknowledge before returning to main menu + Console.ReadKey(); // 等待用户确认后返回主菜单 } static void SelectMissile(ComprehensiveMissileSimulator simulator) @@ -264,8 +250,7 @@ namespace ThreatSource.Tools.MissileSimulation if (choice >= 1 && choice <= indicators.Length) { simulator.ToggleIndicator(indicators[choice - 1].Id); - // Loop continues to show updated status - return; // Return to main menu after toggling sensor + return; } else { @@ -313,8 +298,6 @@ namespace ThreatSource.Tools.MissileSimulation for (int i = 0; i < availableJammingOptions.Length; i++) { var option = availableJammingOptions[i]; - // To get actual status, you'd query simulator: e.g., simulator.IsJammerActive(option.JammerId) - // For now, we use local `jammingStatus` which is temporary for this menu session. Console.WriteLine($" {i + 1}. {option.DisplayName} [{(jammingStatus[i] ? "激活" : "未激活")}] " + $"(干扰模式: {option.Mode}, 作用对象: {option.Target})"); } @@ -339,8 +322,7 @@ namespace ThreatSource.Tools.MissileSimulation else simulator.ClearJamming(option.Type, option.DisplayName, option.JammerId); - // Loop continues - return; // Return to main menu after applying/clearing jamming + return; } else {