增加RCS模型和测试用例,修改 Vector3D为 Struct

This commit is contained in:
Tian jianyong 2025-05-16 18:45:31 +08:00
parent 3c649a753a
commit 3e61c7d044
51 changed files with 2401 additions and 777 deletions

117
RcsPattern_TestFixes.md Normal file
View File

@ -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` 类的当前设计是合理的,不需要修改来"适应"特定的世界坐标系。关键在于调用方正确提供目标局部坐标系下的输入。

View File

@ -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<Vector3D> vectors = new List<Vector3D>(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
}
}
}

View File

@ -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
/// <summary>
/// Helper method to create a test RCS matrix with predictable values.
/// </summary>
/// <param name="rows">Number of rows for the matrix.</param>
/// <param name="cols">Number of columns for the matrix.</param>
/// <param name="valueFunc">Function to generate values. If null, uses (i * 10 + j + 1.0).</param>
/// <returns>A 2D double array representing the RCS matrix.</returns>
private double[,] CreateTestMatrix(int rows = 6, int cols = 4, Func<int, int, double>? 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<List<double>>)
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<List<double>>)
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<ArgumentNullException>(() => new RcsPattern(nullMatrix!));
}
[TestMethod]
public void ParameterizedConstructor_WithInvalidDimensions_ThrowsArgumentException()
{
// Arrange
double[,] wrongRowsMatrix = CreateTestMatrix(5, 4);
double[,] wrongColsMatrix = CreateTestMatrix(6, 3);
// Act & Assert
Assert.ThrowsException<ArgumentException>(() => new RcsPattern(wrongRowsMatrix), "Constructor should throw for wrong row count.");
Assert.ThrowsException<ArgumentException>(() => 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.");
}
}
}

View File

@ -109,7 +109,7 @@ namespace ThreatSource.Tests.Target
public void GetRadarCrossSection_ReturnsExpectedValue() public void GetRadarCrossSection_ReturnsExpectedValue()
{ {
// Arrange // Arrange
var observerPosition = new Vector3D(0, 0, 0); Vector3D observerPosition = new(0, 0, 0);
// Act // Act
double rcs = _tank.Properties.RadarCrossSection; double rcs = _tank.Properties.RadarCrossSection;
@ -122,7 +122,7 @@ namespace ThreatSource.Tests.Target
public void GetInfraredSignature_ReturnsExpectedValue() public void GetInfraredSignature_ReturnsExpectedValue()
{ {
// Arrange // Arrange
var observerPosition = new Vector3D(0, 0, 0); Vector3D observerPosition = new(0, 0, 0);
// Act // Act
double signature = _tank.Properties.InfraredRadiationIntensity; double signature = _tank.Properties.InfraredRadiationIntensity;

View File

@ -67,10 +67,10 @@ namespace ThreatSource.Tests.Jamming
var missileMotion = new KinematicState var missileMotion = new KinematicState
{ {
Position = initialPosition, 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 var missileProperties = new MissileProperties
{ {
MaxSpeed = 1000, MaxSpeed = 1000,

View File

@ -74,15 +74,15 @@ namespace ThreatSource.Tests.Jamming
_testAdapter.AddTestEntity("target1", _target); _testAdapter.AddTestEntity("target1", _target);
// 创建导弹对象 // 创建导弹对象
var initialPosition = new Vector3D(500, 0, 0); Vector3D initialPosition = new(500, 0, 0);
var initialVelocity = new Vector3D(100, 0, 0); Vector3D initialVelocity = new(100, 0, 0);
var missileMotion = new KinematicState KinematicState missileMotion = new()
{ {
Position = initialPosition, 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 var missileProperties = new MissileProperties
{ {
MaxSpeed = 1000, MaxSpeed = 1000,

View File

@ -725,7 +725,7 @@ namespace ThreatSource.Tests.Jamming
} }
// 通过反射获取TargetPosition属性 // 通过反射获取TargetPosition属性
var targetPositionProperty = typeof(LaserSemiActiveGuidanceSystem).GetProperty("TargetPosition", var targetPositionProperty = typeof(LaserSemiActiveGuidanceSystem).GetField("TargetPosition",
System.Reflection.BindingFlags.NonPublic | System.Reflection.BindingFlags.Instance); 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}"); 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}"); Debug.WriteLine($"诱偏目标位置: {decoyPosition}");
// 计算诱偏目标到诱偏源的实际距离 // 计算诱偏目标到诱偏源的实际距离
@ -779,7 +781,8 @@ namespace ThreatSource.Tests.Jamming
Debug.WriteLine($"导弹到真实目标距离: {missileToDReal}米, 导弹到诱偏目标距离: {missileToDDecoy}米"); 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}"); Debug.WriteLine($"合成后的目标位置: {compositePosition}");
// 获取导弹当前的视场角 // 获取导弹当前的视场角
@ -973,7 +976,7 @@ namespace ThreatSource.Tests.Jamming
}; };
// 通过反射获取TargetPosition属性 // 通过反射获取TargetPosition属性
var targetPositionProperty = typeof(LaserSemiActiveGuidanceSystem).GetProperty("TargetPosition", var targetPositionProperty = typeof(LaserSemiActiveGuidanceSystem).GetField("TargetPosition",
System.Reflection.BindingFlags.NonPublic | System.Reflection.BindingFlags.Instance); 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}"); 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}"); Debug.WriteLine($"诱偏目标位置: {decoyPosition}");
// 计算诱偏目标到诱偏源的实际距离 // 计算诱偏目标到诱偏源的实际距离

View File

@ -131,8 +131,8 @@ namespace ThreatSource.Tests.Jamming
Assert.IsNotNull(_missile); Assert.IsNotNull(_missile);
// Arrange // Arrange
var initialPosition = new Vector3D(500, 0, 0); Vector3D initialPosition = new(500, 0, 0);
var initialVelocity = new Vector3D(100, 0, 0); Vector3D initialVelocity = new(100, 0, 0);
System.Diagnostics.Debug.WriteLine($"测试开始 - 初始位置: {initialPosition}, 初始速度: {initialVelocity}"); System.Diagnostics.Debug.WriteLine($"测试开始 - 初始位置: {initialPosition}, 初始速度: {initialVelocity}");
@ -223,8 +223,8 @@ namespace ThreatSource.Tests.Jamming
Assert.IsNotNull(_missile); Assert.IsNotNull(_missile);
// Arrange // Arrange
var initialPosition = new Vector3D(500, 0, 0); Vector3D initialPosition = new(500, 0, 0);
var initialVelocity = new Vector3D(100, 0, 0); Vector3D initialVelocity = new(100, 0, 0);
// 更新导弹位置和速度 // 更新导弹位置和速度
_missile.KState.Position = initialPosition; _missile.KState.Position = initialPosition;
@ -266,8 +266,8 @@ namespace ThreatSource.Tests.Jamming
Assert.IsNotNull(_missile); Assert.IsNotNull(_missile);
// Arrange // Arrange
var initialPosition = new Vector3D(500, 0, 0); Vector3D initialPosition = new(500, 0, 0);
var initialVelocity = new Vector3D(100, 0, 0); Vector3D initialVelocity = new(100, 0, 0);
// 更新导弹位置和速度 // 更新导弹位置和速度
_missile.KState.Position = initialPosition; _missile.KState.Position = initialPosition;
@ -319,8 +319,8 @@ namespace ThreatSource.Tests.Jamming
Assert.IsNotNull(_missile); Assert.IsNotNull(_missile);
// Arrange // Arrange
var initialPosition = new Vector3D(500, 0, 0); Vector3D initialPosition = new(500, 0, 0);
var initialVelocity = new Vector3D(100, 0, 0); Vector3D initialVelocity = new(100, 0, 0);
// 更新导弹位置和速度 // 更新导弹位置和速度
_missile.KState.Position = initialPosition; _missile.KState.Position = initialPosition;
@ -369,8 +369,8 @@ namespace ThreatSource.Tests.Jamming
Assert.IsNotNull(_missile); Assert.IsNotNull(_missile);
// Arrange // Arrange
var initialPosition = new Vector3D(500, 0, 0); Vector3D initialPosition = new(500, 0, 0);
var initialVelocity = new Vector3D(100, 0, 0); Vector3D initialVelocity = new(100, 0, 0);
// 更新导弹位置和速度 // 更新导弹位置和速度
_missile.KState.Position = initialPosition; _missile.KState.Position = initialPosition;

View File

@ -127,8 +127,8 @@ namespace ThreatSource.Tests.Jamming
Assert.IsNotNull(_missile); Assert.IsNotNull(_missile);
// Arrange // Arrange
var initialPosition = new Vector3D(500, 0, 0); Vector3D initialPosition = new(500, 0, 0);
var initialVelocity = new Vector3D(100, 0, 0); Vector3D initialVelocity = new(100, 0, 0);
System.Diagnostics.Debug.WriteLine($"测试开始 - 初始位置: {initialPosition}, 初始速度: {initialVelocity}"); System.Diagnostics.Debug.WriteLine($"测试开始 - 初始位置: {initialPosition}, 初始速度: {initialVelocity}");
@ -176,8 +176,8 @@ namespace ThreatSource.Tests.Jamming
Assert.IsNotNull(_missile); Assert.IsNotNull(_missile);
// Arrange // Arrange
var initialPosition = new Vector3D(500, 0, 0); Vector3D initialPosition = new(500, 0, 0);
var initialVelocity = new Vector3D(100, 0, 0); Vector3D initialVelocity = new(100, 0, 0);
// 更新导弹位置和速度 // 更新导弹位置和速度
_missile.KState.Position = initialPosition; _missile.KState.Position = initialPosition;
@ -218,8 +218,8 @@ namespace ThreatSource.Tests.Jamming
Assert.IsNotNull(_missile); Assert.IsNotNull(_missile);
// Arrange // Arrange
var initialPosition = new Vector3D(500, 0, 0); Vector3D initialPosition = new(500, 0, 0);
var initialVelocity = new Vector3D(100, 0, 0); Vector3D initialVelocity = new(100, 0, 0);
// 更新导弹位置和速度 // 更新导弹位置和速度
_missile.KState.Position = initialPosition; _missile.KState.Position = initialPosition;
@ -258,8 +258,8 @@ namespace ThreatSource.Tests.Jamming
Assert.IsNotNull(_missile); Assert.IsNotNull(_missile);
// Arrange // Arrange
var initialPosition = new Vector3D(500, 0, 0); Vector3D initialPosition = new(500, 0, 0);
var initialVelocity = new Vector3D(100, 0, 0); Vector3D initialVelocity = new(100, 0, 0);
// 更新导弹位置和速度 // 更新导弹位置和速度
_missile.KState.Position = initialPosition; _missile.KState.Position = initialPosition;
@ -311,8 +311,8 @@ namespace ThreatSource.Tests.Jamming
Assert.IsNotNull(_missile); Assert.IsNotNull(_missile);
// Arrange // Arrange
var initialPosition = new Vector3D(500, 0, 0); Vector3D initialPosition = new(500, 0, 0);
var initialVelocity = new Vector3D(100, 0, 0); Vector3D initialVelocity = new(100, 0, 0);
// 更新导弹位置和速度 // 更新导弹位置和速度
_missile.KState.Position = initialPosition; _missile.KState.Position = initialPosition;
@ -361,8 +361,8 @@ namespace ThreatSource.Tests.Jamming
Assert.IsNotNull(_missile); Assert.IsNotNull(_missile);
// Arrange // Arrange
var initialPosition = new Vector3D(500, 0, 0); Vector3D initialPosition = new(500, 0, 0);
var initialVelocity = new Vector3D(100, 0, 0); Vector3D initialVelocity = new(100, 0, 0);
// 更新导弹位置和速度 // 更新导弹位置和速度
_missile.KState.Position = initialPosition; _missile.KState.Position = initialPosition;

View File

@ -188,9 +188,9 @@ namespace ThreatSource.Tests.Jamming
Duration = 60.0, Duration = 60.0,
CloudDiameter = 20.0 CloudDiameter = 20.0
}; };
var initialPosition = new Vector3D(0, 0, 0); Vector3D initialPosition = new(0, 0, 0);
var motionParams = new KinematicState { Position = initialPosition }; KinematicState motionParams = new() { Position = initialPosition };
var smokeGrenade = new SmokeGrenade("test", config, motionParams, _simulationManager); SmokeGrenade smokeGrenade = new("test", config, motionParams, _simulationManager);
smokeGrenade.Activate(); smokeGrenade.Activate();
// Act // Act
@ -299,11 +299,11 @@ namespace ThreatSource.Tests.Jamming
public void CloudSmoke_HighObserverToLowTarget_ThroughSmoke_CalculatesCorrectThickness() public void CloudSmoke_HighObserverToLowTarget_ThroughSmoke_CalculatesCorrectThickness()
{ {
// Arrange // Arrange
var observerPosition = new Vector3D(5, 50, 0); Vector3D observerPosition = new(5, 50, 0);
var targetPosition = new Vector3D(0, 0, 0); Vector3D targetPosition = new(0, 0, 0);
var smokeCenterPosition = new Vector3D(0, 20, 0); Vector3D smokeCenterPosition = new(0, 20, 0);
var config = new SmokeGrenadeConfig SmokeGrenadeConfig config = new()
{ {
SmokeType = SmokeScreenType.Cloud, SmokeType = SmokeScreenType.Cloud,
CloudDiameter = 20.0, CloudDiameter = 20.0,
@ -312,8 +312,8 @@ namespace ThreatSource.Tests.Jamming
Duration = 60.0 Duration = 60.0
}; };
var motionParams = new KinematicState { Position = smokeCenterPosition }; KinematicState motionParams = new() { Position = smokeCenterPosition };
var smokeGrenade = new SmokeGrenade("smoke_high_obs_test", config, motionParams, _simulationManager); SmokeGrenade smokeGrenade = new("smoke_high_obs_test", config, motionParams, _simulationManager);
smokeGrenade.Activate(); smokeGrenade.Activate();

View File

@ -188,14 +188,14 @@ namespace ThreatSource.Tests.Missile
double spiralAngle = 90 * Math.PI / 180; // 转换为弧度 double spiralAngle = 90 * Math.PI / 180; // 转换为弧度
// 计算扫描方向(考虑锥角) // 计算扫描方向(考虑锥角)
Vector3D scanDirection = new Vector3D( Vector3D scanDirection = new(
Math.Cos(spiralAngle) * Math.Sin(30 * Math.PI / 180), Math.Cos(spiralAngle) * Math.Sin(30 * Math.PI / 180),
-Math.Cos(30 * Math.PI / 180), -Math.Cos(30 * Math.PI / 180),
Math.Sin(spiralAngle) * Math.Sin(30 * Math.PI / 180) Math.Sin(spiralAngle) * Math.Sin(30 * Math.PI / 180)
); );
// 计算水平面上的扫描方向 // 计算水平面上的扫描方向
Vector3D horizontalScanDirection = new Vector3D( Vector3D horizontalScanDirection = new(
Math.Cos(spiralAngle), Math.Cos(spiralAngle),
0, 0,
Math.Sin(spiralAngle) Math.Sin(spiralAngle)
@ -262,18 +262,18 @@ namespace ThreatSource.Tests.Missile
try try
{ {
// 设置目标位置 // 设置目标位置
var targetPosition = new Vector3D(targetX, targetY, targetZ); Vector3D targetPosition = new(targetX, targetY, targetZ);
_tank.KState.Position = targetPosition; _tank.KState.Position = targetPosition;
// 设置子弹位置 // 设置子弹位置
var submunitionPosition = new Vector3D(submunitionX, submunitionY, submunitionZ); Vector3D submunitionPosition = new(submunitionX, submunitionY, submunitionZ);
_submunition.KState.Position = submunitionPosition; _submunition.KState.Position = submunitionPosition;
// 计算从子弹到目标的向量 // 计算从子弹到目标的向量
var toTarget = (targetPosition - submunitionPosition).Normalize(); Vector3D toTarget = (targetPosition - submunitionPosition).Normalize();
// 设置子弹的扫描方向直接指向目标 // 设置子弹的扫描方向直接指向目标
var scanDirection = toTarget; Vector3D scanDirection = toTarget;
var field = typeof(TerminalSensitiveSubmunition) var field = typeof(TerminalSensitiveSubmunition)
.GetField("scanDirection", System.Reflection.BindingFlags.NonPublic | System.Reflection.BindingFlags.Instance); .GetField("scanDirection", System.Reflection.BindingFlags.NonPublic | System.Reflection.BindingFlags.Instance);

View File

@ -50,7 +50,7 @@ namespace ThreatSource.Tests.Sensor
{ {
// Arrange // Arrange
double receivedPower = _minDetectablePower * 0.5; // 低于阈值 double receivedPower = _minDetectablePower * 0.5; // 低于阈值
Vector2D spotOffset = new Vector2D(0, 0); // 中心位置 Vector2D spotOffset = new(0, 0); // 中心位置
// Act // Act
_detector.ProcessLaserSignal(receivedPower, spotOffset); _detector.ProcessLaserSignal(receivedPower, spotOffset);
@ -67,7 +67,7 @@ namespace ThreatSource.Tests.Sensor
{ {
// Arrange // Arrange
double receivedPower = _minDetectablePower * 2.0; // 高于阈值 double receivedPower = _minDetectablePower * 2.0; // 高于阈值
Vector2D spotOffset = new Vector2D(0, 0); // 中心位置 Vector2D spotOffset = new(0, 0); // 中心位置
// Act // Act
_detector.ProcessLaserSignal(receivedPower, spotOffset); _detector.ProcessLaserSignal(receivedPower, spotOffset);
@ -89,7 +89,7 @@ namespace ThreatSource.Tests.Sensor
{ {
// Arrange // Arrange
double receivedPower = _minDetectablePower * 10.0; // 充分高于阈值 double receivedPower = _minDetectablePower * 10.0; // 充分高于阈值
Vector2D spotOffset = new Vector2D(offsetX, offsetY); Vector2D spotOffset = new(offsetX, offsetY);
// Act // Act
_detector.ProcessLaserSignal(receivedPower, spotOffset); _detector.ProcessLaserSignal(receivedPower, spotOffset);
@ -105,7 +105,7 @@ namespace ThreatSource.Tests.Sensor
public void GetTargetDirection_NotLocked_ReturnsCurrentDirection() public void GetTargetDirection_NotLocked_ReturnsCurrentDirection()
{ {
// Arrange // Arrange
Vector3D currentDirection = new Vector3D(1, 0, 0); Vector3D currentDirection = new(1, 0, 0);
double sensitivity = 0.5; double sensitivity = 0.5;
// Act - 不调用ProcessLaserSignal保持未锁定状态 // Act - 不调用ProcessLaserSignal保持未锁定状态
@ -119,10 +119,10 @@ namespace ThreatSource.Tests.Sensor
public void GetTargetDirection_Locked_CenterSpot_ReturnsCurrentDirection() public void GetTargetDirection_Locked_CenterSpot_ReturnsCurrentDirection()
{ {
// Arrange // Arrange
Vector3D currentDirection = new Vector3D(1, 0, 0); Vector3D currentDirection = new(1, 0, 0);
double sensitivity = 0.5; double sensitivity = 0.5;
double receivedPower = _minDetectablePower * 10.0; double receivedPower = _minDetectablePower * 10.0;
Vector2D spotOffset = new Vector2D(0, 0); // 中心位置 Vector2D spotOffset = new(0, 0); // 中心位置
// Act // Act
_detector.ProcessLaserSignal(receivedPower, spotOffset); _detector.ProcessLaserSignal(receivedPower, spotOffset);
@ -139,10 +139,10 @@ namespace ThreatSource.Tests.Sensor
public void GetTargetDirection_Locked_RightOffset_CorrectlyAdjusts() public void GetTargetDirection_Locked_RightOffset_CorrectlyAdjusts()
{ {
// Arrange // Arrange
Vector3D currentDirection = new Vector3D(1, 0, 0); Vector3D currentDirection = new(1, 0, 0);
double sensitivity = 0.5; double sensitivity = 0.5;
double receivedPower = _minDetectablePower * 10.0; double receivedPower = _minDetectablePower * 10.0;
Vector2D spotOffset = new Vector2D(0.05, 0); // 右偏,使用更大的偏移 Vector2D spotOffset = new(0.05, 0); // 右偏,使用更大的偏移
// Act // Act
_detector.ProcessLaserSignal(receivedPower, spotOffset); _detector.ProcessLaserSignal(receivedPower, spotOffset);
@ -162,10 +162,10 @@ namespace ThreatSource.Tests.Sensor
public void GetTargetDirection_Locked_UpOffset_CorrectlyAdjusts() public void GetTargetDirection_Locked_UpOffset_CorrectlyAdjusts()
{ {
// Arrange // Arrange
Vector3D currentDirection = new Vector3D(1, 0, 0); Vector3D currentDirection = new(1, 0, 0);
double sensitivity = 0.5; double sensitivity = 0.5;
double receivedPower = _minDetectablePower * 10.0; double receivedPower = _minDetectablePower * 10.0;
Vector2D spotOffset = new Vector2D(0, 0.05); // 上偏,使用更大的偏移 Vector2D spotOffset = new(0, 0.05); // 上偏,使用更大的偏移
// Act // Act
_detector.ProcessLaserSignal(receivedPower, spotOffset); _detector.ProcessLaserSignal(receivedPower, spotOffset);
@ -186,7 +186,7 @@ namespace ThreatSource.Tests.Sensor
{ {
// Arrange // Arrange
double receivedPower = _minDetectablePower * 5.0; double receivedPower = _minDetectablePower * 5.0;
Vector2D spotOffset = new Vector2D(0.005, -0.005); Vector2D spotOffset = new(0.005, -0.005);
_detector.ProcessLaserSignal(receivedPower, spotOffset); _detector.ProcessLaserSignal(receivedPower, spotOffset);
// Act // Act
@ -205,7 +205,7 @@ namespace ThreatSource.Tests.Sensor
{ {
// Arrange // Arrange
double receivedPower = _minDetectablePower * 10.0; double receivedPower = _minDetectablePower * 10.0;
Vector2D spotOffset = new Vector2D(0.05, 0.05); // 极端偏移,超出正常范围 Vector2D spotOffset = new(0.05, 0.05); // 极端偏移,超出正常范围
// Act // Act
_detector.ProcessLaserSignal(receivedPower, spotOffset); _detector.ProcessLaserSignal(receivedPower, spotOffset);
@ -220,9 +220,9 @@ namespace ThreatSource.Tests.Sensor
public void GetTargetDirection_DifferentSensitivity_ProportionalAdjustment() public void GetTargetDirection_DifferentSensitivity_ProportionalAdjustment()
{ {
// Arrange // Arrange
Vector3D currentDirection = new Vector3D(1, 0, 0); Vector3D currentDirection = new(1, 0, 0);
double receivedPower = _minDetectablePower * 10.0; double receivedPower = _minDetectablePower * 10.0;
Vector2D spotOffset = new Vector2D(0.01, 0); // 使用更小的偏移,符合实际情况 Vector2D spotOffset = new(0.01, 0); // 使用更小的偏移,符合实际情况
// 使用更合理的灵敏度值 // 使用更合理的灵敏度值
double standardSensitivity = 0.05; // 实际经验值 double standardSensitivity = 0.05; // 实际经验值
@ -258,9 +258,9 @@ namespace ThreatSource.Tests.Sensor
string testCondition) string testCondition)
{ {
// Arrange // Arrange
Vector3D currentDirection = new Vector3D(1, 0, 0); Vector3D currentDirection = new(1, 0, 0);
double receivedPower = _minDetectablePower * 10.0; double receivedPower = _minDetectablePower * 10.0;
Vector2D offset = new Vector2D(spotOffset, 0); // 水平偏移 Vector2D offset = new(spotOffset, 0); // 水平偏移
// Act // Act
_detector.ProcessLaserSignal(receivedPower, offset); _detector.ProcessLaserSignal(receivedPower, offset);

View File

@ -15,9 +15,9 @@ namespace ThreatSource.Tests.Utils
// 辅助函数,用于断言发射速度接近初始速度 // 辅助函数,用于断言发射速度接近初始速度
private static void AssertLaunchSpeed(Vector3D? launchVelocity, double initialSpeed) private static void AssertLaunchSpeed(Vector3D? launchVelocity, double initialSpeed)
{ {
if (launchVelocity == null) return; // 如果该值为空且不符合预期,则测试已失败 if (!launchVelocity.HasValue) return; // 如果该值为空且不符合预期,则测试已失败
Assert.IsTrue(Math.Abs(launchVelocity.Magnitude() - initialSpeed) < SpeedDelta, Assert.IsTrue(Math.Abs(launchVelocity.Value.Magnitude() - initialSpeed) < SpeedDelta,
$"Expected launch speed close to {initialSpeed}, but got {launchVelocity.Magnitude():F2}. Velocity: {launchVelocity}"); $"Expected launch speed close to {initialSpeed}, but got {launchVelocity.Value.Magnitude():F2}. Velocity: {launchVelocity.Value}");
} }
[TestMethod] [TestMethod]
@ -37,8 +37,8 @@ namespace ThreatSource.Tests.Utils
Assert.IsNotNull(launchVelocity, "Launch velocity should not be null for a reachable target."); Assert.IsNotNull(launchVelocity, "Launch velocity should not be null for a reachable target.");
AssertLaunchSpeed(launchVelocity, initialSpeed); AssertLaunchSpeed(launchVelocity, initialSpeed);
// 我们期望 Y 分量略微为正,以抵消重力和初始阻力的 Y 分量 // 我们期望 Y 分量略微为正,以抵消重力和初始阻力的 Y 分量
Assert.IsTrue(launchVelocity.Y > -Epsilon, $"Expected Y component to be positive or near zero, but got {launchVelocity.Y: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.Z) < Epsilon + 5, $"Expected Z component to be near zero, but got {launchVelocity.Z:F2}"); // 由于阻力矢量影响,允许稍大的容差 Assert.IsTrue(Math.Abs(launchVelocity.Value.Z) < Epsilon + 5, $"Expected Z component to be near zero, but got {launchVelocity.Value.Z:F2}"); // 由于阻力矢量影响,允许稍大的容差
} }
[TestMethod] [TestMethod]
@ -95,7 +95,7 @@ namespace ThreatSource.Tests.Utils
// 断言 // 断言
Assert.IsNotNull(launchVelocity, "Launch velocity should not be null."); Assert.IsNotNull(launchVelocity, "Launch velocity should not be null.");
AssertLaunchSpeed(launchVelocity, initialSpeed); 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() public void CalculateLaunchVelocity_TargetUnreachable_SpeedTooLow_ShouldReturnNull()
{ {
// 准备 // 准备
Vector3D startPos = new Vector3D(0, 0, 0); Vector3D startPos = new(0, 0, 0);
Vector3D targetPos = new Vector3D(10000, 0, 0); // 远距离目标 Vector3D targetPos = new(10000, 0, 0); // 远距离目标
double initialSpeed = 100; // 低速 double initialSpeed = 100; // 低速
Vector3D windVector = Vector3D.Zero; Vector3D windVector = Vector3D.Zero;
@ -120,8 +120,8 @@ namespace ThreatSource.Tests.Utils
public void CalculateLaunchVelocity_StartAndTargetCoincide_InitialSpeedNonZero_ShouldReturnReasonable() public void CalculateLaunchVelocity_StartAndTargetCoincide_InitialSpeedNonZero_ShouldReturnReasonable()
{ {
// 准备 // 准备
Vector3D startPos = new Vector3D(10, 20, 30); Vector3D startPos = new(10, 20, 30);
Vector3D targetPos = new Vector3D(10, 20, 30); Vector3D targetPos = new(10, 20, 30);
double initialSpeed = 50; double initialSpeed = 50;
Vector3D windVector = Vector3D.Zero; Vector3D windVector = Vector3D.Zero;
@ -144,15 +144,15 @@ namespace ThreatSource.Tests.Utils
// 此处initialSpeed 为 50因此不采用特定的 Vector3D.Zero 路径。 // 此处initialSpeed 为 50因此不采用特定的 Vector3D.Zero 路径。
// 它应该会遇到 t_final < 1e-6 的情况并返回 null。 // 它应该会遇到 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] [TestMethod]
public void CalculateLaunchVelocity_StartAndTargetCoincide_InitialSpeedZero_ShouldReturnZeroOrNull() public void CalculateLaunchVelocity_StartAndTargetCoincide_InitialSpeedZero_ShouldReturnZeroOrNull()
{ {
// 准备 // 准备
Vector3D startPos = new Vector3D(0, 0, 0); Vector3D startPos = new(0, 0, 0);
Vector3D targetPos = new Vector3D(0, 0, 0); Vector3D targetPos = new(0, 0, 0);
double initialSpeed = 0; double initialSpeed = 0;
Vector3D windVector = Vector3D.Zero; 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。 // 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。 // T_sol1 = (0 + 0) / (2a) = 0。T_sol2 = (0-0) / (2a) = 0。所以 t_final = 0。
// 然后,如果 deltaPos.MagSq < 1e-9 && initialSpeed < 1e-6则返回 Vector3D.Zero。此条件已满足。 // 然后,如果 deltaPos.MagSq < 1e-9 && initialSpeed < 1e-6则返回 Vector3D.Zero。此条件已满足。
Assert.IsNotNull(launchVelocity, "Launch velocity should not be null."); Assert.IsTrue(launchVelocity.HasValue, "Launch velocity should not be null.");
Assert.AreEqual(0, launchVelocity.MagnitudeSquared(), Epsilon, "Expected Zero vector."); Assert.AreEqual(0, launchVelocity.Value.MagnitudeSquared(), Epsilon, "Expected Zero vector.");
} }
@ -175,8 +175,8 @@ namespace ThreatSource.Tests.Utils
public void CalculateLaunchVelocity_VerticalShot_NoWind_ShouldHit() public void CalculateLaunchVelocity_VerticalShot_NoWind_ShouldHit()
{ {
// 准备 // 准备
Vector3D startPos = new Vector3D(0, 0, 0); Vector3D startPos = new(0, 0, 0);
Vector3D targetPos = new Vector3D(0, 100, 0); // 目标正上方 Vector3D targetPos = new(0, 100, 0); // 目标正上方
double initialSpeed = 150; // 足够的速度 double initialSpeed = 150; // 足够的速度
Vector3D windVector = Vector3D.Zero; 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."); Assert.IsNotNull(launchVelocity, "Launch velocity should not be null for a vertical shot.");
AssertLaunchSpeed(launchVelocity, initialSpeed); AssertLaunchSpeed(launchVelocity, initialSpeed);
Assert.IsTrue(launchVelocity.Y > 0, $"Expected Y component to be positive, got {launchVelocity.Y:F2}"); Assert.IsTrue(launchVelocity.HasValue); // 确保 launchVelocity.Value 可以安全访问
Assert.IsTrue(Math.Abs(launchVelocity.X) < Epsilon + 0.1, $"Expected X component to be near zero, got {launchVelocity.X:F2}"); // 由于阻力计算,允许一些小的值 Assert.IsTrue(launchVelocity.Value.Y > 0, $"Expected Y component to be positive, got {launchVelocity.Value.Y:F2}");
Assert.IsTrue(Math.Abs(launchVelocity.Z) < Epsilon + 0.1, $"Expected Z component to be near zero, got {launchVelocity.Z: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] [TestMethod]
public void CalculateLaunchVelocity_WithInitialGuess_ShouldProduceValidResult() public void CalculateLaunchVelocity_WithInitialGuess_ShouldProduceValidResult()
{ {
// 准备 // 准备
Vector3D startPos = new Vector3D(0, 0, 0); Vector3D startPos = new(0, 0, 0);
Vector3D targetPos = new Vector3D(500, 50, 0); Vector3D targetPos = new(500, 50, 0);
double initialSpeed = 200; 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; // 一个合理的猜测 Vector3D initialGuess = new Vector3D(150, 100, 10).Normalize() * initialSpeed; // 一个合理的猜测
// 执行 // 执行

View File

@ -16,12 +16,12 @@ namespace ThreatSource.Tests.Utils
public void CalculateProjectedOverlapRatio_ForegroundBehindBackground_ReturnsZero() public void CalculateProjectedOverlapRatio_ForegroundBehindBackground_ReturnsZero()
{ {
// Arrange // Arrange
Vector3D observerPos = new Vector3D(0, 0, -10); Vector3D observerPos = new(0, 0, -10);
Vector3D backgroundCenter = new Vector3D(0, 0, 0); Vector3D backgroundCenter = new(0, 0, 0);
Vector3D backgroundDims = new Vector3D(2, 2, 2); Vector3D backgroundDims = new(2, 2, 2);
Orientation backgroundOrient = IdentityOrientation(); Orientation backgroundOrient = IdentityOrientation();
Vector3D foregroundCenter = new Vector3D(0, 0, 5); // Foreground is behind background Vector3D foregroundCenter = new(0, 0, 5); // Foreground is behind background
Vector3D foregroundDims = new Vector3D(1, 1, 1); Vector3D foregroundDims = new(1, 1, 1);
Orientation foregroundOrient = IdentityOrientation(); Orientation foregroundOrient = IdentityOrientation();
// Act // Act
@ -37,12 +37,12 @@ namespace ThreatSource.Tests.Utils
public void CalculateProjectedOverlapRatio_ForegroundToTheSide_ReturnsZero() public void CalculateProjectedOverlapRatio_ForegroundToTheSide_ReturnsZero()
{ {
// Arrange // Arrange
Vector3D observerPos = new Vector3D(0, 0, -10); Vector3D observerPos = new(0, 0, -10);
Vector3D backgroundCenter = new Vector3D(0, 0, 0); Vector3D backgroundCenter = new(0, 0, 0);
Vector3D backgroundDims = new Vector3D(2, 2, 2); Vector3D backgroundDims = new(2, 2, 2);
Orientation backgroundOrient = IdentityOrientation(); Orientation backgroundOrient = IdentityOrientation();
Vector3D foregroundCenter = new Vector3D(5, 0, 0); // Foreground is to the side Vector3D foregroundCenter = new(5, 0, 0); // Foreground is to the side
Vector3D foregroundDims = new Vector3D(1, 1, 1); Vector3D foregroundDims = new(1, 1, 1);
Orientation foregroundOrient = IdentityOrientation(); Orientation foregroundOrient = IdentityOrientation();
// Act // Act
@ -58,12 +58,12 @@ namespace ThreatSource.Tests.Utils
public void CalculateProjectedOverlapRatio_ObserverInsideForeground_ReturnsOne() public void CalculateProjectedOverlapRatio_ObserverInsideForeground_ReturnsOne()
{ {
// Arrange // Arrange
Vector3D observerPos = new Vector3D(0, 0, 0); // Observer inside foreground Vector3D observerPos = new(0, 0, 0); // Observer inside foreground
Vector3D backgroundCenter = new Vector3D(0, 0, 10); Vector3D backgroundCenter = new(0, 0, 10);
Vector3D backgroundDims = new Vector3D(2, 2, 2); Vector3D backgroundDims = new(2, 2, 2);
Orientation backgroundOrient = IdentityOrientation(); Orientation backgroundOrient = IdentityOrientation();
Vector3D foregroundCenter = new Vector3D(0, 0, 0); Vector3D foregroundCenter = new(0, 0, 0);
Vector3D foregroundDims = new Vector3D(4, 4, 4); // Foreground larger than background Vector3D foregroundDims = new(4, 4, 4); // Foreground larger than background
Orientation foregroundOrient = IdentityOrientation(); Orientation foregroundOrient = IdentityOrientation();
// Act // Act
@ -107,12 +107,12 @@ namespace ThreatSource.Tests.Utils
public void CalculateProjectedOverlapRatio_IdenticalObjectsAligned_ReturnsOne() public void CalculateProjectedOverlapRatio_IdenticalObjectsAligned_ReturnsOne()
{ {
// Arrange // Arrange
Vector3D observerPos = new Vector3D(0, 0, -10); Vector3D observerPos = new(0, 0, -10);
Vector3D backgroundCenter = new Vector3D(0, 0, 0); Vector3D backgroundCenter = new(0, 0, 0);
Vector3D backgroundDims = new Vector3D(2, 2, 2); Vector3D backgroundDims = new(2, 2, 2);
Orientation backgroundOrient = IdentityOrientation(); Orientation backgroundOrient = IdentityOrientation();
Vector3D foregroundCenter = new Vector3D(0, 0, -5); // Foreground between observer and background Vector3D foregroundCenter = new(0, 0, -5); // Foreground between observer and background
Vector3D foregroundDims = new Vector3D(2, 2, 2); // Same size Vector3D foregroundDims = new(2, 2, 2); // Same size
Orientation foregroundOrient = IdentityOrientation(); Orientation foregroundOrient = IdentityOrientation();
// Act // Act
@ -130,17 +130,17 @@ namespace ThreatSource.Tests.Utils
public void CalculateProjectedOverlapRatio_PartialOverlap_Horizontal_ReturnsHalf() public void CalculateProjectedOverlapRatio_PartialOverlap_Horizontal_ReturnsHalf()
{ {
// Arrange: Foreground covers half of the background horizontally // Arrange: Foreground covers half of the background horizontally
Vector3D observerPos = new Vector3D(0, 0, -10); Vector3D observerPos = new(0, 0, -10);
Vector3D backgroundCenter = new Vector3D(0, 0, 0); Vector3D backgroundCenter = new(0, 0, 0);
// 维度更新为 (宽度 Width=1, 高度 Height=2, 长度 Length=4) // 维度更新为 (宽度 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(); Orientation backgroundOrient = IdentityOrientation();
// 前景中心 X 坐标调整为 -0.5,使其投影 U 范围为 [-1.0, 0.0] // 前景中心 X 坐标调整为 -0.5,使其投影 U 范围为 [-1.0, 0.0]
// 与背景 U 范围 [-0.5, 0.5] 重叠于 [-0.5, 0.0],宽度为 0.5 (背景宽度 1.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) // 维度更新为 (宽度 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(); Orientation foregroundOrient = IdentityOrientation();
// Act // Act
@ -158,17 +158,17 @@ namespace ThreatSource.Tests.Utils
{ {
// Arrange // Arrange
// 将观察者移到 Z 轴负方向,确保前景在观察者和背景之间 // 将观察者移到 Z 轴负方向,确保前景在观察者和背景之间
var observerPos = new Vector3D(0, 0, -10); Vector3D observerPos = new(0, 0, -10);
var backgroundCenter = Vector3D.Zero; Vector3D backgroundCenter = Vector3D.Zero;
// 维度现在表示 (宽度 Width=1, 高度 Height=2, 长度 Length=4) // 维度现在表示 (宽度 Width=1, 高度 Height=2, 长度 Length=4)
var backgroundDims = new Vector3D(1.0, 2.0, 4.0); Vector3D backgroundDims = new(1.0, 2.0, 4.0);
var backgroundOrient = new Orientation(0, 0, 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) // 维度现在表示 (宽度 Width=1, 高度 Height=2, 长度 Length=2)
var foregroundDims = new Vector3D(1.0, 2.0, 2.0); Vector3D foregroundDims = new(1.0, 2.0, 2.0);
var foregroundOrient = new Orientation(0, 0, 0); Orientation foregroundOrient = new(0, 0, 0);
// Act // Act
double ratio = ObscurationUtils.CalculateProjectedOverlapRatio( double ratio = ObscurationUtils.CalculateProjectedOverlapRatio(
@ -184,19 +184,19 @@ namespace ThreatSource.Tests.Utils
public void CalculateProjectedOverlapRatio_PartialOverlap_Horizontal_ReturnsThreeQuarters() public void CalculateProjectedOverlapRatio_PartialOverlap_Horizontal_ReturnsThreeQuarters()
{ {
// Arrange: 前景覆盖背景宽度的 75% // Arrange: 前景覆盖背景宽度的 75%
Vector3D observerPos = new Vector3D(0, 0, -10); Vector3D observerPos = new(0, 0, -10);
Vector3D backgroundCenter = new Vector3D(0, 0, 0); Vector3D backgroundCenter = new(0, 0, 0);
// 背景宽度 = 4, 高度 = 2 // 背景宽度 = 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); Orientation backgroundOrient = new Orientation(0, 0, 0);
// 前景宽度 = 3, 高度 = 2, 中心 X 偏移 -0.5 // 前景宽度 = 3, 高度 = 2, 中心 X 偏移 -0.5
// U 范围: [-0.5 - 1.5, -0.5 + 1.5] = [-2.0, 1.0] // U 范围: [-0.5 - 1.5, -0.5 + 1.5] = [-2.0, 1.0]
// 背景 U 范围: [-2.0, 2.0] // 背景 U 范围: [-2.0, 2.0]
// 重叠宽度 = 3.0 // 重叠宽度 = 3.0
Vector3D foregroundCenter = new Vector3D(-0.5, 0.0, -5.0); Vector3D foregroundCenter = new(-0.5, 0.0, -5.0);
Vector3D foregroundDims = new Vector3D(3.0, 2.0, 1.0); // W=3, H=2, L=1 Vector3D foregroundDims = new(3.0, 2.0, 1.0); // W=3, H=2, L=1
Orientation foregroundOrient = new Orientation(0, 0, 0); Orientation foregroundOrient = new(0, 0, 0);
// Act // Act
double ratio = ObscurationUtils.CalculateProjectedOverlapRatio( double ratio = ObscurationUtils.CalculateProjectedOverlapRatio(
@ -214,19 +214,19 @@ namespace ThreatSource.Tests.Utils
public void CalculateProjectedOverlapRatio_PartialOverlap_Vertical_ReturnsNineTenths() public void CalculateProjectedOverlapRatio_PartialOverlap_Vertical_ReturnsNineTenths()
{ {
// Arrange: 前景覆盖背景高度的 90% // Arrange: 前景覆盖背景高度的 90%
Vector3D observerPos = new Vector3D(0, 0, -10); Vector3D observerPos = new(0, 0, -10);
Vector3D backgroundCenter = new Vector3D(0, 0, 0); Vector3D backgroundCenter = new(0, 0, 0);
// 背景宽度 = 2, 高度 = 10 // 背景宽度 = 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); Orientation backgroundOrient = new Orientation(0, 0, 0);
// 前景宽度 = 2, 高度 = 9, 中心 Y 偏移 -0.5 // 前景宽度 = 2, 高度 = 9, 中心 Y 偏移 -0.5
// V 范围: [-0.5 - 4.5, -0.5 + 4.5] = [-5.0, 4.0] // V 范围: [-0.5 - 4.5, -0.5 + 4.5] = [-5.0, 4.0]
// 背景 V 范围: [-5.0, 5.0] // 背景 V 范围: [-5.0, 5.0]
// 重叠高度 = 9.0 // 重叠高度 = 9.0
Vector3D foregroundCenter = new Vector3D(0.0, -0.5, -5.0); Vector3D foregroundCenter = new(0.0, -0.5, -5.0);
Vector3D foregroundDims = new Vector3D(2.0, 9.0, 1.0); // W=2, H=9, L=1 Vector3D foregroundDims = new(2.0, 9.0, 1.0); // W=2, H=9, L=1
Orientation foregroundOrient = new Orientation(0, 0, 0); Orientation foregroundOrient = new(0, 0, 0);
// Act // Act
double ratio = ObscurationUtils.CalculateProjectedOverlapRatio( double ratio = ObscurationUtils.CalculateProjectedOverlapRatio(
@ -244,17 +244,17 @@ namespace ThreatSource.Tests.Utils
public void CalculateProjectedOverlapRatio_FarObserver_RuntimeScenario_ShouldBeOne() public void CalculateProjectedOverlapRatio_FarObserver_RuntimeScenario_ShouldBeOne()
{ {
// Arrange: 使用运行时观察到的参数 // Arrange: 使用运行时观察到的参数
Vector3D observerPos = new Vector3D(2100.0, 1.2, 0.0); // 远距离观察者 Vector3D observerPos = new(2100.0, 1.2, 0.0); // 远距离观察者
// 背景 (目标: Tank_1) // 背景 (目标: Tank_1)
Vector3D backgroundCenter = new Vector3D(0.0, 1.2, 0.0); Vector3D backgroundCenter = new(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 Vector3D backgroundDims = new(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) Orientation backgroundOrient = new(Math.PI/2, 0.0, 0.0); // Yaw=180 deg (原Yaw=PI)
// 前景 (烟幕: SG_2) // 前景 (烟幕: SG_2)
Vector3D foregroundCenter = new Vector3D(50.0, 5.0, 0.0); Vector3D foregroundCenter = new(50.0, 5.0, 0.0);
Vector3D foregroundDims = new Vector3D(5.0, 10.0, 50.0); Vector3D foregroundDims = new(5.0, 10.0, 50.0);
Orientation foregroundOrient = new Orientation(0.0, 0.0, 0.0); Orientation foregroundOrient = new(0.0, 0.0, 0.0);
// Act // Act
double ratio = ObscurationUtils.CalculateProjectedOverlapRatio( double ratio = ObscurationUtils.CalculateProjectedOverlapRatio(

View File

@ -13,7 +13,7 @@ Width = 3.5 # 宽度 (米)
Height = 2.4 # 高度 (米) Height = 2.4 # 高度 (米)
MaxSpeed = 70.0 # 最大速度 (千米/小时) MaxSpeed = 70.0 # 最大速度 (千米/小时)
ArmorThickness = 800.0 # 装甲厚度 (毫米) ArmorThickness = 800.0 # 装甲厚度 (毫米)
RadarCrossSection = 15.0 # 雷达散射截面积 (平方米) RadarCrossSection = 15.0 # 雷达散射截面积 (平方米) - 保留作为通用/回退值
InfraredRadiationIntensity = 250.0 # 红外辐射强度 (瓦特/球面度) InfraredRadiationIntensity = 250.0 # 红外辐射强度 (瓦特/球面度)
UltravioletRadiationIntensity = 15.0 # 紫外辐射强度 (瓦特/球面度) UltravioletRadiationIntensity = 15.0 # 紫外辐射强度 (瓦特/球面度)
MillimeterWaveRadiationIntensity = 10.0 # 毫米波辐射强度 (瓦特/球面度) MillimeterWaveRadiationIntensity = 10.0 # 毫米波辐射强度 (瓦特/球面度)
@ -30,4 +30,34 @@ MovingPatternSource = [ # 移动时温度分布
[45, 50, 85], [45, 50, 85],
[40, 45, 95], [40, 45, 95],
[65, 65, 75] [65, 65, 75]
] ]
[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)
]

View File

@ -31,7 +31,7 @@ namespace ThreatSource.Data
private readonly Dictionary<string, MissileData> _missiles = []; private readonly Dictionary<string, MissileData> _missiles = [];
private readonly Dictionary<string, IndicatorData> _indicators = []; private readonly Dictionary<string, IndicatorData> _indicators = [];
private readonly Dictionary<string, SensorData> _sensors = []; private readonly Dictionary<string, SensorData> _sensors = [];
private readonly Dictionary<string, EquipmentData> _targets = []; private readonly Dictionary<string, EquipmentData> _equipments = [];
private readonly Dictionary<string, WeatherData> _weathers = []; private readonly Dictionary<string, WeatherData> _weathers = [];
private readonly Dictionary<string, JammerData> _jammers = []; private readonly Dictionary<string, JammerData> _jammers = [];
/// <summary> /// <summary>
@ -55,7 +55,7 @@ namespace ThreatSource.Data
LoadMissiles(Path.Combine(path, "missiles")); LoadMissiles(Path.Combine(path, "missiles"));
LoadIndicators(Path.Combine(path, "indicators")); LoadIndicators(Path.Combine(path, "indicators"));
LoadSensors(Path.Combine(path, "sensors")); LoadSensors(Path.Combine(path, "sensors"));
LoadTargets(Path.Combine(path, "targets")); LoadTargets(Path.Combine(path, "equipments"));
LoadWeathers(Path.Combine(path, "weathers")); LoadWeathers(Path.Combine(path, "weathers"));
LoadJammers(Path.Combine(path, "jammers")); LoadJammers(Path.Combine(path, "jammers"));
} }
@ -177,7 +177,7 @@ namespace ThreatSource.Data
if (data != null) if (data != null)
{ {
string model = Path.GetFileNameWithoutExtension(file); string model = Path.GetFileNameWithoutExtension(file);
_targets[model] = data; _equipments[model] = data;
} }
} }
catch (Exception ex) catch (Exception ex)
@ -292,9 +292,9 @@ namespace ThreatSource.Data
/// </summary> /// </summary>
/// <param name="model">目标型号</param> /// <param name="model">目标型号</param>
/// <returns>目标配置数据</returns> /// <returns>目标配置数据</returns>
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; return data;
throw new KeyNotFoundException($"目标 {model} 不存在"); throw new KeyNotFoundException($"目标 {model} 不存在");
} }
@ -342,7 +342,7 @@ namespace ThreatSource.Data
/// <summary> /// <summary>
/// 获取所有可用的目标ID列表 /// 获取所有可用的目标ID列表
/// </summary> /// </summary>
public IEnumerable<string> GetAvailableTargets() => _targets.Keys; public IEnumerable<string> GetAvailableTargets() => _equipments.Keys;
/// <summary> /// <summary>
/// 获取所有可用的天气ID列表 /// 获取所有可用的天气ID列表

View File

@ -173,9 +173,9 @@ namespace ThreatSource.Data
/// <param name="targetModel">目标型号</param> /// <param name="targetModel">目标型号</param>
/// <param name="motionParameters">初始运动参数</param> /// <param name="motionParameters">初始运动参数</param>
/// <returns>目标实例</returns> /// <returns>目标实例</returns>
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 return data.Type switch
{ {

View File

@ -50,7 +50,6 @@ namespace ThreatSource.Equipment
: base(id, motionParameters, simulationManager) : base(id, motionParameters, simulationManager)
{ {
Properties = equipmentProperties; Properties = equipmentProperties;
} }
/// <summary> /// <summary>

View File

@ -1,4 +1,5 @@
using ThreatSource.Utils; using ThreatSource.Utils;
using ThreatSource.Equipment; // Ensure RcsPattern and ThermalPattern are findable
namespace ThreatSource.Equipment namespace ThreatSource.Equipment
{ {
@ -162,6 +163,14 @@ namespace ThreatSource.Equipment
/// </remarks> /// </remarks>
public ThermalPattern ThermalPattern { get; set; } public ThermalPattern ThermalPattern { get; set; }
/// <summary>
/// 获取或设置目标的雷达散射截面(RCS)分布模式
/// </summary>
/// <remarks>
/// 描述目标的雷达回波特征分布包含角度依赖的RCS数据。
/// </remarks>
public RcsPattern RcsPattern { get; set; }
/// <summary> /// <summary>
/// 初始化装备属性类的新实例 /// 初始化装备属性类的新实例
/// </summary> /// </summary>
@ -174,8 +183,11 @@ namespace ThreatSource.Equipment
{ {
SetDefaultValues(); SetDefaultValues();
Type = "Tank"; // 默认类型为坦克 Type = "Tank"; // 默认类型为坦克
// 初始化一个默认的温度分布模式 ThermalPattern = new ThermalPattern(
ThermalPattern = new ThermalPattern(new double[3, 3], new double[3, 3]); 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
} }
/// <summary> /// <summary>

View File

@ -0,0 +1,102 @@
using System;
namespace ThreatSource.Equipment
{
/// <summary>
/// 定义特定角度区间的雷达散射截面(RCS)数据。
/// </summary>
public class RcsAngularBin
{
/// <summary>
/// 角度区间描述名称 (如 "前方", "左侧上方")。
/// </summary>
public string AspectName { get; set; } = "Default";
/// <summary>
/// 水平起始角度 (度)。
/// </summary>
public double HorAngleStartDeg { get; set; }
/// <summary>
/// 水平终止角度 (度)。
/// </summary>
public double HorAngleEndDeg { get; set; }
/// <summary>
/// 俯仰起始角度 (度)。
/// </summary>
public double VerAngleStartDeg { get; set; }
/// <summary>
/// 俯仰终止角度 (度)。
/// </summary>
public double VerAngleEndDeg { get; set; }
/// <summary>
/// 雷达散射截面值 (dBsm)。
/// </summary>
public double RcsValueDbSm { get; set; }
/// <summary>
/// RCS值 (平方米 m^2)。
/// </summary>
public double RcsValueM2 => Math.Pow(10, RcsValueDbSm / 10.0);
/// <summary>
/// 检查给定角度是否在此RCS数据仓内。
/// </summary>
/// <param name="azimuthDeg">方位角 (度)</param>
/// <param name="elevationDeg">俯仰角 (度)</param>
/// <returns>若角度在仓内则为true否则为false。</returns>
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;
}
/// <summary>
/// 构造函数。
/// </summary>
public RcsAngularBin()
{
AspectName = "Default";
}
}
}

View File

@ -0,0 +1,405 @@
using ThreatSource.Utils; // 假设 Vector3D 在此命名空间下
using System.Linq; // For Average, if used later
using System.Collections.Generic; // Required for List<T>
using System; // Required for ArgumentNullException, ArgumentException, InvalidOperationException
using System.Diagnostics; // Required for Debug.WriteLine
namespace ThreatSource.Equipment
{
/// <summary>
/// 管理目标的雷达散射截面(RCS)角度依赖数据。
/// 使用一个6x4矩阵存储RCS值对应目标的前、后、左、右、上、下六个主方向
/// 以及每个主方向下根据观察者局部视角划分的四个象限。
/// </summary>
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
/// <summary>
/// RCS数据矩阵的源数据 (主要供Tomlyn等TOML解析器使用)。
/// TOML配置文件中的 'RcsMatrix.Data' 字段将映射到此属性。
/// 代表一个6x4的RCS值矩阵 (dBsm)。
/// </summary>
public List<List<double>> RcsMatrixDataSource { get; set; }
/// <summary>
/// RCS数据矩阵 (6x4, 单位 dBsm)。通过按需转换 <see cref="RcsMatrixDataSource"/> 获取。
/// 行定义: 0-前, 1-后, 2-左, 3-右, 4-上, 5-下。
/// 列定义 (象限): 基于观察者相对于当前主方向局部参考轴的左右和上下位置确定。
/// - 列0: 局部右, 局部上
/// - 列1: 局部左, 局部上
/// - 列2: 局部左, 局部下
/// - 列3: 局部右, 局部下
/// </summary>
public double[,] RcsMatrixData
{
get { return ConvertSourceToGrid(RcsMatrixDataSource, nameof(RcsMatrixDataSource)); }
}
/// <summary>
/// 默认构造函数。初始化一个6x4的RCS数据源所有值设为默认值 (<see cref="DEFAULT_RCS_DBSM"/>)。
/// 主要供Tomlyn等反序列化工具使用。
/// </summary>
public RcsPattern()
{
RcsMatrixDataSource = new List<List<double>>(RCS_MATRIX_ROWS);
for (int i = 0; i < RCS_MATRIX_ROWS; i++)
{
var row = new List<double>(RCS_MATRIX_COLS);
for (int j = 0; j < RCS_MATRIX_COLS; j++)
{
row.Add(DEFAULT_RCS_DBSM);
}
RcsMatrixDataSource.Add(row);
}
}
/// <summary>
/// 构造函数用于从外部提供的6x4二维数组初始化RCS模式。
/// </summary>
/// <param name="rcsMatrix">一个6x4的二维数组包含RCS值 (dBsm)。</param>
/// <exception cref="ArgumentNullException">如果 `rcsMatrix` 为 null。</exception>
/// <exception cref="ArgumentException">如果 `rcsMatrix` 的维度不是6x4。</exception>
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<List<double>> 转换为 double[,]
private double[,] ConvertSourceToGrid(List<List<double>> 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<List<double>>
private List<List<double>> 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<List<double>>(RCS_MATRIX_ROWS);
for(int i=0; i < RCS_MATRIX_ROWS; i++)
{
var row = new List<double>(RCS_MATRIX_COLS);
for(int j=0; j < RCS_MATRIX_COLS; j++)
{
row.Add(grid[i,j]);
}
source.Add(row);
}
return source;
}
/// <summary>
/// 根据视线方向的全局方位角和俯仰角获取RCS值 (dBsm)。
/// <remarks>
/// 注意:此方法使用简化的、基于全局角度的象限确定逻辑。
/// 其象限划分可能与向量版本不完全一致,特别是在处理目标后部等复杂情况时。
/// 对于需要精确局部视角RCS值的场景推荐使用 <see cref="GetRcsValueDbSm(Vector3D, Vector3D, Vector3D, Vector3D)"/> 版本。
/// </remarks>
/// </summary>
/// <param name="azimuthDeg">方位角 (度),相对于目标全局前方参考线 (-180到180度)。</param>
/// <param name="elevationDeg">俯仰角 (度),相对于目标全局水平面 (-90到90度)。</param>
/// <returns>匹配的RCS值 (dBsm)。</returns>
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;
}
}
/// <summary>
/// 根据目标自身坐标系中的观察向量计算精确的局部视角RCS值 (dBsm)。
/// <remarks>
/// 此方法通过计算观察者相对于目标各主方向局部参考轴的精确几何关系来确定RCS矩阵的行和列
/// 提供了比基于全局角度的重载方法 <see cref="GetRcsValueDbSm(double, double)"/> 更精确的局部视角RCS值。
/// </remarks>
/// </summary>
/// <param name="observerDirectionInTargetFrame">观察者方向向量 (从目标指向观察者,在目标坐标系中,将被归一化)。</param>
/// <param name="targetForwardVectorInTargetFrame">目标"前方"向量 (在目标坐标系中,通常为(1,0,0)或(0,0,1)等)。</param>
/// <param name="targetUpVectorInTargetFrame">目标"上方"向量 (在目标坐标系中,通常为(0,1,0)或(0,0,1)等)。</param>
/// <param name="targetRightVectorInTargetFrame">目标\"右方\"向量 (在目标坐标系中)。</param>
/// <returns>RCS值 (dBsm)。</returns>
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;
}
}
/// <summary>
/// 计算RCS矩阵中所有值的算术平均值。
/// </summary>
/// <returns>平均RCS值 (dBsm)。</returns>
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;
}
}
}

View File

@ -331,7 +331,7 @@ namespace ThreatSource.Guidance
if (parameters.Mode == JammingMode.Blocking) if (parameters.Mode == JammingMode.Blocking)
{ {
HasGuidance = false; // 硬干扰立即停止制导 HasGuidance = false; // 硬干扰立即停止制导
Trace.TraceInformation($"[{this.GetType().Name}] 硬干扰已应用: {parameters.Type}"); Trace.TraceInformation($"[{GetType().Name}] 硬干扰已应用: {parameters.Type}");
} }
} }

View File

@ -182,13 +182,13 @@ namespace ThreatSource.Guidance
return; return;
} }
Vector3D currentDesiredDirection = (LastTrackerToTargetVector - LastTrackerToMissileVector).Normalize(); Vector3D currentDesiredDirection = (LastTrackerToTargetVector.Value - LastTrackerToMissileVector.Value).Normalize();
Vector3D currentDirection = KState.Velocity.Normalize(); Vector3D currentDirection = KState.Velocity.Normalize();
if (LastDesiredDirection != null && deltaTime > 0) 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; turnRate = turnRate * (1 - TurnRateSmoothingFactor) + instantTurnRate * TurnRateSmoothingFactor;
} }
else else
@ -196,7 +196,7 @@ namespace ThreatSource.Guidance
turnRate = 0; 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 desiredDirectionWithLead = (currentDesiredDirection + leadDirection * turnRate * LeadTimeFactor).Normalize();
Vector3D turnAxis = Vector3D.CrossProduct(currentDirection, desiredDirectionWithLead).Normalize(); Vector3D turnAxis = Vector3D.CrossProduct(currentDirection, desiredDirectionWithLead).Normalize();
double turnAngle = Vector3D.AngleBetween(currentDirection, desiredDirectionWithLead); double turnAngle = Vector3D.AngleBetween(currentDirection, desiredDirectionWithLead);
@ -222,7 +222,7 @@ namespace ThreatSource.Guidance
// 添加角度信息 // 添加角度信息
if (LastTrackerToTargetVector != null && LastTrackerToMissileVector != null) if (LastTrackerToTargetVector != null && LastTrackerToMissileVector != null)
{ {
double angle = Vector3D.AngleBetween(LastTrackerToTargetVector, LastTrackerToMissileVector); double angle = Vector3D.AngleBetween(LastTrackerToTargetVector.Value, LastTrackerToMissileVector.Value);
statusInfo.ExtendedProperties["TurnAngle"] = angle; statusInfo.ExtendedProperties["TurnAngle"] = angle;
} }
else else

View File

@ -494,7 +494,7 @@ namespace ThreatSource.Guidance
const double ObscurationThreshold = 0.8; // 80%遮挡视为完全遮挡 const double ObscurationThreshold = 0.8; // 80%遮挡视为完全遮挡
Vector3D targetCenter = target.KState.Position; Vector3D targetCenter = target.KState.Position;
Orientation targetOrient = target.KState.Orientation; 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) foreach (var smoke in smokeGrenades)
{ {
@ -580,7 +580,7 @@ namespace ThreatSource.Guidance
public override ElementStatusInfo GetStatusInfo() public override ElementStatusInfo GetStatusInfo()
{ {
var statusInfo = base.GetStatusInfo(); var statusInfo = base.GetStatusInfo();
string lastPosStr = lastTargetPosition != null ? lastTargetPosition.ToString() : "null"; string lastPosStr = lastTargetPosition.HasValue ? lastTargetPosition.Value.ToString() : "null";
statusInfo.ExtendedProperties["lastPosStr"] = lastPosStr; statusInfo.ExtendedProperties["lastPosStr"] = lastPosStr;
return statusInfo; return statusInfo;
} }

View File

@ -407,7 +407,7 @@ namespace ThreatSource.Guidance
Debug.WriteLine($"激光驾束引导系统: 在控制场内, 距离: {shortestDistance}"); Debug.WriteLine($"激光驾束引导系统: 在控制场内, 距离: {shortestDistance}");
Vector3D missileToSource = LaserSourcePosition - KState.Position; Vector3D missileToSource = LaserSourcePosition.Value - KState.Position;
double distance = missileToSource.Magnitude(); double distance = missileToSource.Magnitude();
double receivedPower = CalculateReceivedLaserPower(distance); double receivedPower = CalculateReceivedLaserPower(distance);
@ -510,7 +510,7 @@ namespace ThreatSource.Guidance
lateralAcceleration = lateralAcceleration.Normalize() * config.MaxGuidanceAcceleration; lateralAcceleration = lateralAcceleration.Normalize() * config.MaxGuidanceAcceleration;
} }
Vector3D forwardDirection = LaserDirection; Vector3D forwardDirection = LaserDirection.Value;
Vector3D currentDirection = KState.Velocity.Normalize(); Vector3D currentDirection = KState.Velocity.Normalize();
Vector3D rotationAxis = Vector3D.CrossProduct(currentDirection, forwardDirection); Vector3D rotationAxis = Vector3D.CrossProduct(currentDirection, forwardDirection);
double rotationAngle = Math.Acos(Vector3D.DotProduct(currentDirection, forwardDirection)); double rotationAngle = Math.Acos(Vector3D.DotProduct(currentDirection, forwardDirection));
@ -589,8 +589,8 @@ namespace ThreatSource.Guidance
public override ElementStatusInfo GetStatusInfo() public override ElementStatusInfo GetStatusInfo()
{ {
var statusInfo = base.GetStatusInfo(); var statusInfo = base.GetStatusInfo();
string sourcePosStr = LaserSourcePosition != null ? LaserSourcePosition.ToString() : "null"; string sourcePosStr = LaserSourcePosition.HasValue ? LaserSourcePosition.Value.ToString() : "null";
string directionStr = LaserDirection != null ? LaserDirection.ToString() : "null"; string directionStr = LaserDirection.HasValue ? LaserDirection.Value.ToString() : "null";
statusInfo.ExtendedProperties["SourcePos"] = sourcePosStr; statusInfo.ExtendedProperties["SourcePos"] = sourcePosStr;
statusInfo.ExtendedProperties["Direction"] = directionStr; statusInfo.ExtendedProperties["Direction"] = directionStr;
@ -618,9 +618,9 @@ namespace ThreatSource.Guidance
return Vector3D.Zero; return Vector3D.Zero;
} }
Vector3D missileToSource = LaserSourcePosition - KState.Position; Vector3D missileToSource = LaserSourcePosition.Value - KState.Position;
double projectionLength = Vector3D.DotProduct(missileToSource, LaserDirection); double projectionLength = Vector3D.DotProduct(missileToSource, LaserDirection.Value);
Vector3D closestPointOnBeam = LaserSourcePosition - LaserDirection * projectionLength; Vector3D closestPointOnBeam = LaserSourcePosition.Value - LaserDirection.Value * projectionLength;
Vector3D shortestDistanceVector = closestPointOnBeam - KState.Position; Vector3D shortestDistanceVector = closestPointOnBeam - KState.Position;
return shortestDistanceVector; return shortestDistanceVector;

View File

@ -253,7 +253,7 @@ namespace ThreatSource.Guidance
{ {
laserTargets.RemoveAt(existingIndex); laserTargets.RemoveAt(existingIndex);
} }
laserTargets.Add((target, evt.SpotPosition, laserDesignator)); laserTargets.Add((target, evt.SpotPosition.Value, laserDesignator));
// 只要收到有效事件就认为照射开启 (有效性由后续SNR判断) // 只要收到有效事件就认为照射开启 (有效性由后续SNR判断)
LaserIlluminationOn = true; LaserIlluminationOn = true;
@ -590,7 +590,7 @@ namespace ThreatSource.Guidance
} }
// No cast needed, just use TargetPosition directly // 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(); Vector3D currentDirection = KState.Velocity.Normalize();

View File

@ -572,7 +572,7 @@ namespace ThreatSource.Guidance
if (LastTargetPosition != null) if (LastTargetPosition != null)
{ {
double proximityThreshold = (target.Properties.Length > 0) ? target.Properties.Length * 5.0 : 100.0; 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) if (IsTargetInBeam(missileVelocity, toTarget) && isPotentiallyTrackedTarget)
@ -664,8 +664,8 @@ namespace ThreatSource.Guidance
{ {
var statusInfo = base.GetStatusInfo(); var statusInfo = base.GetStatusInfo();
string lastPosStr = LastTargetPosition != null ? LastTargetPosition.ToString() : "null"; string lastPosStr = LastTargetPosition.HasValue ? LastTargetPosition.Value.ToString() : "null";
string lastVelStr = LastTargetVelocity != null ? LastTargetVelocity.ToString() : "null"; string lastVelStr = LastTargetVelocity.HasValue ? LastTargetVelocity.Value.ToString() : "null";
statusInfo.ExtendedProperties["Mode"] = currentMode.ToString(); statusInfo.ExtendedProperties["Mode"] = currentMode.ToString();
statusInfo.ExtendedProperties["LastTargetPosition"] = lastPosStr; statusInfo.ExtendedProperties["LastTargetPosition"] = lastPosStr;
@ -723,7 +723,7 @@ namespace ThreatSource.Guidance
weatherSpecificAtmosphericTransmittance = AtmosphereDllWrapper.CalculateTransmittance( weatherSpecificAtmosphericTransmittance = AtmosphereDllWrapper.CalculateTransmittance(
distance, distance,
RadiationType.MillimeterWave, RadiationType.MillimeterWave,
wavelength_um, // Use wavelength_um wavelength_um,
SimulationManager.CurrentWeather); SimulationManager.CurrentWeather);
} }
signalPower *= weatherSpecificAtmosphericTransmittance; signalPower *= weatherSpecificAtmosphericTransmittance;
@ -738,18 +738,13 @@ namespace ThreatSource.Guidance
double snr_dB_no_smoke = 10 * Math.Log10(snr_linear); 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}"); 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 if (smokeTransmittanceLinear <= 1e-6) // 防止log(0)或非常大的负数
// 应用烟幕透过率 (smokeTransmittanceLinear is already linear 0-1)
if (smokeTransmittanceLinear <= 0.000001) // Threshold to prevent log(0) or very large negative numbers
{ {
Debug.WriteLine($"[SNR计算] 烟幕透过率太低. Final_SNR_dB: -100.0 dB"); 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 smokeEffect_dB = 10 * Math.Log10(smokeTransmittanceLinear);
double final_snr_dB = snr_dB_no_smoke + smokeEffect_dB; double final_snr_dB = snr_dB_no_smoke + smokeEffect_dB;
Debug.WriteLine($"[SNR计算] 烟幕影响: {smokeEffect_dB:F2} dB. 最终SNR_dB (烟幕后): {final_snr_dB:F2} 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); double transmittanceForThisSmoke = smokeGrenade.GetSmokeTransmittanceOnLine(observerPosition, targetEndPosition, wavelength_um);
totalTransmittance *= transmittanceForThisSmoke; totalTransmittance *= transmittanceForThisSmoke;
if (totalTransmittance < 0.000001) // 使用更小的阈值提前退出,如果透过率几乎为零 if (totalTransmittance < 1e-6) // 使用更小的阈值提前退出,如果透过率几乎为零
{ {
return 0.0; return 0.0;
} }

View File

@ -158,7 +158,7 @@ namespace ThreatSource.Indicator
/// <param name="parameters">干扰参数</param> /// <param name="parameters">干扰参数</param>
protected virtual void HandleJammingApplied(JammingParameters parameters) 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");
} }
/// <summary> /// <summary>
@ -167,7 +167,7 @@ namespace ThreatSource.Indicator
/// <param name="parameters">干扰参数</param> /// <param name="parameters">干扰参数</param>
protected virtual void HandleJammingCleared(JammingParameters parameters) 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");
} }
/// <summary> /// <summary>
@ -403,7 +403,7 @@ namespace ThreatSource.Indicator
protected virtual bool ShouldHandleJamming(JammingParameters parameters) protected virtual bool ShouldHandleJamming(JammingParameters parameters)
{ {
// 首先检查当前指示器实例(通过其具体的 SupportedJammingTypes 实现)是否支持此干扰类型 // 首先检查当前指示器实例(通过其具体的 SupportedJammingTypes 实现)是否支持此干扰类型
if (!this.SupportedJammingTypes.Contains(parameters.Type)) if (!SupportedJammingTypes.Contains(parameters.Type))
{ {
return false; return false;
} }

View File

@ -131,7 +131,7 @@ namespace ThreatSource.Indicator
// 目标被遮挡,尝试使用最后已知位置 // 目标被遮挡,尝试使用最后已知位置
if (_lastKnownTargetPosition != null) if (_lastKnownTargetPosition != null)
{ {
currentTargetPosition = _lastKnownTargetPosition; currentTargetPosition = _lastKnownTargetPosition.Value;
Debug.WriteLine($"红外测角仪 {Id}: 目标被遮挡,使用最后已知位置 {currentTargetPosition}"); Debug.WriteLine($"红外测角仪 {Id}: 目标被遮挡,使用最后已知位置 {currentTargetPosition}");
} }
else else

View File

@ -353,7 +353,7 @@ namespace ThreatSource.Jammer
Vector3D.DotProduct(rayOriginRelativeToOBBCenter_w, obbAxisZ_w) 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, obbAxisX_w),
Vector3D.DotProduct(rayDirection_world, obbAxisY_w), Vector3D.DotProduct(rayDirection_world, obbAxisY_w),
Vector3D.DotProduct(rayDirection_world, obbAxisZ_w) Vector3D.DotProduct(rayDirection_world, obbAxisZ_w)

View File

@ -206,7 +206,7 @@ namespace ThreatSource.Missile
/// </remarks> /// </remarks>
public void LightInfraredSource() public void LightInfraredSource()
{ {
SimulationManager.PublishEvent(new InfraredGuidanceMissileLightEvent { RadiationPower = this.RadiationPower, SenderId = this.Id }); SimulationManager.PublishEvent(new InfraredGuidanceMissileLightEvent { RadiationPower = RadiationPower, SenderId = Id });
} }
/// <summary> /// <summary>
@ -232,7 +232,7 @@ namespace ThreatSource.Missile
/// </remarks> /// </remarks>
public void LightOffInfraredSource() public void LightOffInfraredSource()
{ {
SimulationManager.PublishEvent(new InfraredGuidanceMissileLightOffEvent { SenderId = this.Id }); SimulationManager.PublishEvent(new InfraredGuidanceMissileLightOffEvent { SenderId = Id });
} }
/// <summary> /// <summary>

View File

@ -144,7 +144,7 @@ namespace ThreatSource.Missile
//计算此时目标的预期位置 //计算此时目标的预期位置
Vector3D targetPredictPosition = target.KState.Position + target.KState.Velocity * timeToScanPoint; 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; Vector3D windVec = Vector3D.Zero;
@ -170,8 +170,8 @@ namespace ThreatSource.Missile
} }
else else
{ {
KState.Velocity = launchVelocityNullable; KState.Velocity = launchVelocityNullable.Value;
KState.Orientation = Orientation.FromVector(launchVelocityNullable); KState.Orientation = Orientation.FromVector(launchVelocityNullable.Value);
} }
} }
@ -316,7 +316,7 @@ namespace ThreatSource.Missile
Vector3D scanTargetPosition = targetPredictPosition + new Vector3D(0, submunitionConfig.StableScanHeight, 0); Vector3D scanTargetPosition = targetPredictPosition + new Vector3D(0, submunitionConfig.StableScanHeight, 0);
// 计算子弹的初始方向 // 计算子弹的初始方向
Vector3D? initialDirection = MotionAlgorithm.CalculateLaunchVelocityWithSimplifiedDrag( Vector3D? initialDirectionNullable = MotionAlgorithm.CalculateLaunchVelocityWithSimplifiedDrag(
KState.Position, KState.Position,
scanTargetPosition, scanTargetPosition,
KState.Speed, KState.Speed,
@ -325,14 +325,12 @@ namespace ThreatSource.Missile
null null
); );
if (initialDirection == null) if (initialDirectionNullable == null)
{ {
throw new InvalidOperationException("无法计算子弹的初始方向"); throw new InvalidOperationException("无法计算子弹的初始方向");
} }
else
{ Vector3D initialDirection = initialDirectionNullable.Value.Normalize();
initialDirection = initialDirection.Normalize();
}
// 根据新方向创建朝向 // 根据新方向创建朝向
Orientation separationOrientation = Orientation.FromVector(initialDirection); Orientation separationOrientation = Orientation.FromVector(initialDirection);

View File

@ -117,7 +117,7 @@ namespace ThreatSource.Sensor
var wavelength = config.Band == InfraredBand.Long ? 10.0 : 4.0; // 代表性波长 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; // 累乘透射率 smokeAttenuation *= singleSmokeTransmittance; // 累乘透射率
} }
} }

View File

@ -123,7 +123,7 @@ namespace ThreatSource.Sensor
var wavelength = config.Wavelength; var wavelength = config.Wavelength;
// 计算此烟幕的透射率 // 计算此烟幕的透射率
double singleSmokeTransmittance = smokeGrenade.GetSmokeTransmittanceOnLine(KState.Position, targetPointForLos, wavelength); double singleSmokeTransmittance = smokeGrenade.GetSmokeTransmittanceOnLine(KState.Position, targetPointForLos.Value, wavelength);
smokeAttenuation *= singleSmokeTransmittance; // 累乘透射率 smokeAttenuation *= singleSmokeTransmittance; // 累乘透射率
} }
} }
@ -260,9 +260,9 @@ namespace ThreatSource.Sensor
if (targetPoint != null) if (targetPoint != null)
{ {
// 计算到目标点的斜距 // 计算到目标点的斜距
double dx = targetPoint.X - KState.Position.X; double dx = targetPoint.Value.X - KState.Position.X;
double dy = targetPoint.Y - KState.Position.Y; double dy = targetPoint.Value.Y - KState.Position.Y;
double dz = targetPoint.Z - KState.Position.Z; double dz = targetPoint.Value.Z - KState.Position.Z;
double calculatedSlantRange = Math.Sqrt(dx * dx + dy * dy + dz * dz); double calculatedSlantRange = Math.Sqrt(dx * dx + dy * dy + dz * dz);
// 确保距离不超过最大探测范围 // 确保距离不超过最大探测范围

View File

@ -149,9 +149,9 @@ namespace ThreatSource.Sensor
if (SimulationManager.GetEntityById(jammerId) is SmokeGrenade smokeGrenade) 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 var wavelength = config.Band switch
{ {
@ -160,7 +160,7 @@ namespace ThreatSource.Sensor
_ => 0.0 _ => 0.0
}; };
// 计算此烟幕的透射率 // 计算此烟幕的透射率
double singleSmokeTransmittance = smokeGrenade.GetSmokeTransmittanceOnLine(KState.Position, targetPointForLos, wavelength); double singleSmokeTransmittance = smokeGrenade.GetSmokeTransmittanceOnLine(KState.Position, targetPointForLos.Value, wavelength);
smokeAttenuation *= singleSmokeTransmittance; // 累乘透射率 smokeAttenuation *= singleSmokeTransmittance; // 累乘透射率
} }
} }

View File

@ -162,9 +162,9 @@ namespace ThreatSource.Sensor
if (SimulationManager.GetEntityById(jammerId) is SmokeGrenade smokeGrenade) 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 var wavelength = config.Band switch
{ {
@ -173,7 +173,7 @@ namespace ThreatSource.Sensor
_ => 0.0 _ => 0.0
}; };
// 计算此烟幕的透射率 // 计算此烟幕的透射率
double singleSmokeTransmittance = smokeGrenade.GetSmokeTransmittanceOnLine(KState.Position, targetPointForLos, wavelength); double singleSmokeTransmittance = smokeGrenade.GetSmokeTransmittanceOnLine(KState.Position, targetPointForLos.Value, wavelength);
smokeAttenuation *= singleSmokeTransmittance; // 累乘透射率 smokeAttenuation *= singleSmokeTransmittance; // 累乘透射率
} }
} }

View File

@ -224,13 +224,13 @@ namespace ThreatSource.Sensor
// 简化模型:根据光斑中心到各象限中心的距离计算信号强度 // 简化模型:根据光斑中心到各象限中心的距离计算信号强度
// 象限中心坐标 // 象限中心坐标
double quadrantRadius = detectorRadius / 2; 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), // 左上
new Vector2D(-quadrantRadius, -quadrantRadius), // 左下 new Vector2D(-quadrantRadius, -quadrantRadius), // 左下
new Vector2D(quadrantRadius, -quadrantRadius) // 右下 new Vector2D(quadrantRadius, -quadrantRadius) // 右下
}; ];
// 计算每个象限接收到的信号强度 // 计算每个象限接收到的信号强度
double totalWeight = 0; double totalWeight = 0;

View File

@ -50,7 +50,7 @@ namespace ThreatSource.Simulation
set set
{ {
_speed = value; _speed = value;
if (_velocity != null && _velocity.MagnitudeSquared() > 1e-9) // 容差,避免浮点问题 if (_velocity.MagnitudeSquared() > 1e-9) // 容差,避免浮点问题
{ {
// 保持现有方向,更新模长 // 保持现有方向,更新模长
_velocity = _velocity.Normalize() * _speed; _velocity = _velocity.Normalize() * _speed;
@ -81,7 +81,7 @@ namespace ThreatSource.Simulation
get => _velocity; get => _velocity;
set set
{ {
_velocity = value ?? Vector3D.Zero; //确保 _velocity 不为 null _velocity = value;
_speed = _velocity.Magnitude(); _speed = _velocity.Magnitude();
} }
} }
@ -148,7 +148,7 @@ namespace ThreatSource.Simulation
{ {
Position = position; Position = position;
Velocity = velocity; // Setter 会同时更新 Speed Velocity = velocity; // Setter 会同时更新 Speed
if (velocity != null && velocity.MagnitudeSquared() > 1e-9) if (velocity.MagnitudeSquared() > 1e-9)
{ {
Orientation = Orientation.FromVector(velocity.Normalize()); Orientation = Orientation.FromVector(velocity.Normalize());
} }

View File

@ -1,466 +1,5 @@
using System;
using System.Numerics;
namespace ThreatSource.Utils namespace ThreatSource.Utils
{ {
/// <summary>
/// 表示三维空间中的向量,提供基本的向量运算功能
/// </summary>
/// <remarks>
/// 该类实现了三维向量的基本运算,包括:
/// - 向量的加减乘除运算
/// - 向量的点积和叉积
/// - 向量的归一化
/// - 向量的旋转变换
/// 所有计算都使用双精度浮点数double以保证精度
/// </remarks>
public class Vector3D
{
/// <summary>
/// X轴单位向量 (1, 0, 0)
/// </summary>
public static Vector3D UnitX => new Vector3D(1, 0, 0);
/// <summary>
/// Y轴单位向量 (0, 1, 0)
/// </summary>
public static Vector3D UnitY => new Vector3D(0, 1, 0);
/// <summary>
/// Z轴单位向量 (0, 0, 1)
/// </summary>
public static Vector3D UnitZ => new Vector3D(0, 0, 1);
/// <summary>
/// 零向量 (0, 0, 0)
/// </summary>
public static Vector3D Zero => new Vector3D(0, 0, 0);
/// <summary>
/// 用于浮点数比较的小量阈值
/// </summary>
private const double EPSILON = 1e-10;
/// <summary>
/// 获取或设置向量的X分量
/// </summary>
public double X { get; set; }
/// <summary>
/// 获取或设置向量的Y分量
/// </summary>
public double Y { get; set; }
/// <summary>
/// 获取或设置向量的Z分量
/// </summary>
public double Z { get; set; }
/// <summary>
/// 初始化三维向量的新实例
/// </summary>
/// <param name="x">X分量的值</param>
/// <param name="y">Y分量的值</param>
/// <param name="z">Z分量的值</param>
public Vector3D(double x, double y, double z)
{
X = x;
Y = y;
Z = z;
}
/// <summary>
/// 计算两个向量之间的距离
/// </summary>
/// <param name="v1">第一个向量</param>
/// <param name="v2">第二个向量</param>
/// <returns>两个向量之间的距离</returns>
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));
}
/// <summary>
/// 将向量转换为字符串表示
/// </summary>
/// <returns>向量的字符串表示</returns>
public override string ToString()
{
return $"({X:F2}, {Y:F2}, {Z:F2})";
}
/// <summary>
/// 向量减法运算符重载
/// </summary>
public static Vector3D operator -(Vector3D a, Vector3D b)
{
return new Vector3D(a.X - b.X, a.Y - b.Y, a.Z - b.Z);
}
/// <summary>
/// 向量加法运算符重载
/// </summary>
public static Vector3D operator +(Vector3D a, Vector3D b)
{
return new Vector3D(a.X + b.X, a.Y + b.Y, a.Z + b.Z);
}
/// <summary>
/// 向量与标量乘法运算符重载
/// </summary>
public static Vector3D operator *(Vector3D a, double scalar)
{
return new Vector3D(a.X * scalar, a.Y * scalar, a.Z * scalar);
}
/// <summary>
/// 向量与标量除法运算符重载
/// </summary>
public static Vector3D operator /(Vector3D a, double scalar)
{
return new Vector3D(a.X / scalar, a.Y / scalar, a.Z / scalar);
}
/// <summary>
/// 向量反向
/// </summary>
public static Vector3D operator -(Vector3D a)
{
return new Vector3D(-a.X, -a.Y, -a.Z);
}
/// <summary>
/// 向量相等运算符重载
/// </summary>
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;
}
/// <summary>
/// 向量不相等运算符重载
/// </summary>
public static bool operator !=(Vector3D? left, Vector3D? right)
{
return !(left == right);
}
/// <summary>
/// 判断两个向量是否相等
/// </summary>
public override bool Equals(object? obj)
{
if (obj is Vector3D other)
{
return this == other;
}
return false;
}
/// <summary>
/// 获取向量的哈希码
/// </summary>
/// <returns>向量的哈希码</returns>
public override int GetHashCode()
{
return HashCode.Combine(X, Y, Z);
}
/// <summary>
/// 计算向量的模长
/// </summary>
/// <returns>向量的模长</returns>
public double Magnitude()
{
return Math.Sqrt(X * X + Y * Y + Z * Z);
}
/// <summary>
/// 计算向量模长的平方
/// </summary>
/// <returns>向量模长的平方</returns>
public double MagnitudeSquared()
{
return X * X + Y * Y + Z * Z;
}
/// <summary>
/// 向量归一化
/// </summary>
/// <returns>归一化后的向量</returns>
/// <remarks>
/// 如果向量的模长小于 EPSILON则返回零向量
/// 否则返回单位向量
/// </remarks>
public Vector3D Normalize()
{
double mag = Magnitude();
if (mag > EPSILON)
{
return new Vector3D(X / mag, Y / mag, Z / mag);
}
return Vector3D.Zero;
}
/// <summary>
/// 检查向量是否为单位向量
/// </summary>
/// <param name="tolerance">允许的误差范围,默认为 EPSILON</param>
/// <returns>如果向量是单位向量则返回 true否则返回 false</returns>
/// <remarks>
/// 通过检查向量的模长平方是否在 1.0 的 tolerance 范围内来判断
/// </remarks>
public bool IsUnit(double tolerance = EPSILON)
{
return Math.Abs(MagnitudeSquared() - 1.0) < tolerance;
}
/// <summary>
/// 计算两个向量的叉积
/// </summary>
/// <param name="a">第一个向量</param>
/// <param name="b">第二个向量</param>
/// <returns>叉积结果</returns>
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
);
}
/// <summary>
/// 计算两个向量的点积
/// </summary>
/// <param name="a">第一个向量</param>
/// <param name="b">第二个向量</param>
/// <returns>点积结果</returns>
public static double DotProduct(Vector3D a, Vector3D b)
{
return a.X * b.X + a.Y * b.Y + a.Z * b.Z;
}
/// <summary>
/// 向量取反
/// </summary>
/// <param name="a">输入向量</param>
/// <returns>取反后的向量</returns>
public static Vector3D Negate(Vector3D a)
{
return new Vector3D(-a.X, -a.Y, -a.Z);
}
/// <summary>
/// 计算AB两点之间距离 A 点一定距离的点的坐标
/// </summary>
/// <param name="a">直线起点</param>
/// <param name="b">直线终点</param>
/// <param name="distance">距离起点距离</param>
/// <returns>直线上的点</returns>
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;
}
/// <summary>
/// 计算旋转矩阵
/// </summary>
/// <param name="orientation">方向</param>
/// <param name="misalignmentAngle">失调角</param>
/// <returns>旋转矩阵</returns>
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);
}
/// <summary>
/// 使用矩阵变换方向向量(不应用平移)
/// </summary>
/// <param name="direction">要变换的方向向量</param>
/// <param name="matrix">变换矩阵</param>
/// <returns>变换后的方向向量</returns>
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);
}
/// <summary>
/// 计算两个向量之间的角度
/// </summary>
/// <param name="a">第一个向量</param>
/// <param name="b">第二个向量</param>
/// <returns>两个向量之间的角度(弧度)</returns>
/// <remarks>
/// 该方法会先对向量进行归一化,然后计算它们之间的夹角。
/// 结果总是在 [0, π] 范围内。
/// </remarks>
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);
}
}
/// <summary>
/// 表示三维空间中的方向,使用欧拉角表示
/// </summary>
/// <remarks>
/// 使用欧拉角(偏航角、俯仰角、滚转角)来表示三维空间中的方向:
/// - 偏航角Yaw绕Y轴旋转的角度
/// - 俯仰角Pitch绕X轴旋转的角度
/// - 滚转角Roll绕Z轴旋转的角度
/// 所有角度均使用弧度制
/// </remarks>
public struct Orientation
{
/// <summary>
/// 获取或设置偏航角绕Y轴旋转的角度单位弧度
/// </summary>
public double Yaw { get; set; }
/// <summary>
/// 获取或设置俯仰角绕X轴旋转的角度单位弧度
/// </summary>
public double Pitch { get; set; }
/// <summary>
/// 获取或设置滚转角绕Z轴旋转的角度单位弧度
/// </summary>
public double Roll { get; set; }
/// <summary>
/// 初始化方向的新实例
/// </summary>
/// <param name="yaw">偏航角,单位:弧度</param>
/// <param name="pitch">俯仰角,单位:弧度</param>
/// <param name="roll">滚转角,单位:弧度</param>
public Orientation(double yaw, double pitch, double roll)
{
Yaw = yaw;
Pitch = pitch;
Roll = roll;
}
/// <summary>
/// 将方向转换为字符串表示
/// </summary>
/// <returns>方向的字符串表示</returns>
public override readonly string ToString()
{
return $"(Yaw: {Yaw:F2}, Pitch: {Pitch:F2}, Roll: {Roll:F2})";
}
/// <summary>
/// 将角度归一化到 [-π, π] 范围内
/// </summary>
public void Normalize()
{
Yaw = NormalizeAngle(Yaw);
Pitch = NormalizeAngle(Pitch);
Roll = NormalizeAngle(Roll);
}
/// <summary>
/// 将单个角度归一化到 [-π, π] 范围内
/// </summary>
/// <param name="angle">输入角度</param>
/// <returns>归一化后的角度</returns>
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();
}
/// <summary>
/// 将方向转换为单位向量
/// </summary>
/// <returns>对应的单位向量</returns>
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);
}
/// <summary>
/// 从向量创建方向 (基于向量代表期望的 -Z 前向)
/// </summary>
/// <param name="worldForwardVector">输入的世界前向单位向量</param>
/// <returns>对应的方向 (RHS/CCW 欧拉角)</returns>
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
}
}
/// <summary> /// <summary>
/// 表示二维平面上的向量 /// 表示二维平面上的向量
/// </summary> /// </summary>
@ -493,7 +32,7 @@ namespace ThreatSource.Utils
/// <summary> /// <summary>
/// 零向量 (0, 0) /// 零向量 (0, 0)
/// </summary> /// </summary>
public static Vector2D Zero => new Vector2D(0, 0); public static Vector2D Zero => new(0, 0);
} }
/// <summary> /// <summary>

View File

@ -394,38 +394,21 @@ namespace ThreatSource.Utils
Vector3D? initialGuessVelocityForDrag = null Vector3D? initialGuessVelocityForDrag = null
) )
{ {
Vector3D effectiveInitialVelocityForDragEstimation; Vector3D targetDirection = (targetPos - startPos).Normalize();
if (initialGuessVelocityForDrag != null && initialGuessVelocityForDrag.MagnitudeSquared() > 1e-9) Vector3D currentVelocity = targetDirection * initialSpeed;
{
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 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 deltaPos = targetPos - startPos;
Vector3D gravityVector = new(0, -Gravity, 0); Vector3D gravityVector = new(0, -Gravity, 0);
Vector3D effectiveTotalAcceleration = gravityVector + constantDragAccelerationVector; Vector3D effectiveTotalAcceleration = gravityVector + dragAcceleration;
double coeff_a = 0.25 * effectiveTotalAcceleration.MagnitudeSquared(); double coeff_a = 0.25 * effectiveTotalAcceleration.MagnitudeSquared();
double coeff_b = -(Vector3D.DotProduct(deltaPos, effectiveTotalAcceleration) + initialSpeed * initialSpeed); double coeff_b = -(Vector3D.DotProduct(deltaPos, effectiveTotalAcceleration) + initialSpeed * initialSpeed);

View File

@ -0,0 +1,235 @@
using System; // Mathのため、System名前空間が必要
namespace ThreatSource.Utils
{
/// <summary>
/// 表示一个3x3的旋转矩阵。
/// </summary>
public readonly struct Matrix3x3
{
/// <summary>M11</summary>
public readonly double M11;
/// <summary>M12</summary>
public readonly double M12;
/// <summary>M13</summary>
public readonly double M13;
/// <summary>M21</summary>
public readonly double M21;
/// <summary>M22</summary>
public readonly double M22;
/// <summary>M23</summary>
public readonly double M23;
/// <summary>M31</summary>
public readonly double M31;
/// <summary>M32</summary>
public readonly double M32;
/// <summary>M33</summary>
public readonly double M33;
/// <summary>
/// 初始化 <see cref="Matrix3x3"/> 结构体的新实例。
/// </summary>
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;
}
/// <summary>
/// 获取单位矩阵。
/// </summary>
public static Matrix3x3 Identity { get; } = new Matrix3x3(
1, 0, 0,
0, 1, 0,
0, 0, 1
);
/// <summary>
/// 通过此矩阵变换指定的向量。
/// </summary>
/// <param name="vector">要变换的向量。</param>
/// <returns>变换后的向量。</returns>
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
);
}
/// <summary>
/// 计算并返回此矩阵的转置。
/// </summary>
/// <returns>转置后的矩阵。</returns>
/// <remarks>对于旋转矩阵,转置矩阵即为其逆矩阵。</remarks>
public readonly Matrix3x3 Transpose()
{
return new Matrix3x3(
M11, M21, M31,
M12, M22, M32,
M13, M23, M33
);
}
}
/// <summary>
/// 表示三维空间中的方向,使用欧拉角表示
/// </summary>
/// <remarks>
/// 使用欧拉角(偏航角、俯仰角、滚转角)来表示三维空间中的方向:
/// - 偏航角Yaw绕Y轴旋转的角度
/// - 俯仰角Pitch绕X轴旋转的角度
/// - 滚转角Roll绕Z轴旋转的角度
/// 所有角度均使用弧度制
/// </remarks>
public struct Orientation
{
/// <summary>
/// 获取或设置偏航角绕Y轴旋转的角度单位弧度
/// </summary>
public double Yaw { get; set; }
/// <summary>
/// 获取或设置俯仰角绕X轴旋转的角度单位弧度
/// </summary>
public double Pitch { get; set; }
/// <summary>
/// 获取或设置滚转角绕Z轴旋转的角度单位弧度
/// </summary>
public double Roll { get; set; }
/// <summary>
/// 初始化方向的新实例
/// </summary>
/// <param name="yaw">偏航角,单位:弧度</param>
/// <param name="pitch">俯仰角,单位:弧度</param>
/// <param name="roll">滚转角,单位:弧度</param>
public Orientation(double yaw, double pitch, double roll)
{
Yaw = yaw;
Pitch = pitch;
Roll = roll;
}
/// <summary>
/// 将方向转换为字符串表示
/// </summary>
/// <returns>方向的字符串表示</returns>
public override readonly string ToString()
{
return $"(Yaw: {Yaw:F2}, Pitch: {Pitch:F2}, Roll: {Roll:F2})";
}
/// <summary>
/// 将角度归一化到 [-π, π] 范围内
/// </summary>
public void Normalize()
{
Yaw = NormalizeAngle(Yaw);
Pitch = NormalizeAngle(Pitch);
Roll = NormalizeAngle(Roll);
}
/// <summary>
/// 将单个角度归一化到 [-π, π] 范围内
/// </summary>
/// <param name="angle">输入角度</param>
/// <returns>归一化后的角度</returns>
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();
}
/// <summary>
/// 将方向转换为表示从局部坐标系到父坐标系的旋转矩阵。
/// 旋转顺序为 Rz(Roll) -> Rx(Pitch) -> Ry(Yaw)。
/// 与ToVector() (-Z向前)的约定兼容。
/// </summary>
/// <returns>3x3的旋转矩阵。</returns>
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
);
}
/// <summary>
/// 将方向转换为表示从父坐标系到局部坐标系的旋转矩阵。
/// 这是 ToRotationMatrix() 的逆变换。
/// </summary>
/// <returns>3x3的旋转矩阵 (世界到局部)。</returns>
public readonly Matrix3x3 ToWorldToLocalRotationMatrix()
{
return ToRotationMatrix().Transpose();
}
/// <summary>
/// 将方向转换为单位向量
/// </summary>
/// <returns>对应的单位向量</returns>
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);
}
/// <summary>
/// 从向量创建方向 (基于向量代表期望的 -Z 前向)
/// </summary>
/// <param name="worldForwardVector">输入的世界前向单位向量</param>
/// <returns>对应的方向 (RHS/CCW 欧拉角)</returns>
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
}
}
}

View File

@ -0,0 +1,343 @@
using System.Numerics;
namespace ThreatSource.Utils
{
/// <summary>
/// 表示三维空间中的向量,提供基本的向量运算功能
/// </summary>
/// <remarks>
/// 该类实现了三维向量的基本运算,包括:
/// - 向量的加减乘除运算
/// - 向量的点积和叉积
/// - 向量的归一化
/// - 向量的旋转变换
/// 所有计算都使用双精度浮点数double以保证精度
/// </remarks>
public struct Vector3D
{
/// <summary>
/// X轴单位向量 (1, 0, 0)
/// </summary>
public static Vector3D UnitX => new(1, 0, 0);
/// <summary>
/// Y轴单位向量 (0, 1, 0)
/// </summary>
public static Vector3D UnitY => new(0, 1, 0);
/// <summary>
/// Z轴单位向量 (0, 0, 1)
/// </summary>
public static Vector3D UnitZ => new(0, 0, 1);
/// <summary>
/// 零向量 (0, 0, 0)
/// </summary>
public static Vector3D Zero => new(0, 0, 0);
/// <summary>
/// 用于浮点数比较的小量阈值
/// </summary>
private const double EPSILON = 1e-10;
/// <summary>
/// 获取或设置向量的X分量
/// </summary>
public double X { get; set; }
/// <summary>
/// 获取或设置向量的Y分量
/// </summary>
public double Y { get; set; }
/// <summary>
/// 获取或设置向量的Z分量
/// </summary>
public double Z { get; set; }
/// <summary>
/// 初始化三维向量的新实例
/// </summary>
/// <param name="x">X分量的值</param>
/// <param name="y">Y分量的值</param>
/// <param name="z">Z分量的值</param>
public Vector3D(double x, double y, double z)
{
X = x;
Y = y;
Z = z;
}
/// <summary>
/// 计算两个向量之间的距离
/// </summary>
/// <param name="v1">第一个向量</param>
/// <param name="v2">第二个向量</param>
/// <returns>两个向量之间的距离</returns>
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);
}
/// <summary>
/// 将向量转换为字符串表示
/// </summary>
/// <returns>向量的字符串表示</returns>
public override readonly string ToString()
{
return $"({X:F2}, {Y:F2}, {Z:F2})";
}
/// <summary>
/// 向量减法运算符重载
/// </summary>
public static Vector3D operator -(Vector3D a, Vector3D b)
{
return new Vector3D(a.X - b.X, a.Y - b.Y, a.Z - b.Z);
}
/// <summary>
/// 向量加法运算符重载
/// </summary>
public static Vector3D operator +(Vector3D a, Vector3D b)
{
return new Vector3D(a.X + b.X, a.Y + b.Y, a.Z + b.Z);
}
/// <summary>
/// 向量与标量乘法运算符重载
/// </summary>
public static Vector3D operator *(Vector3D a, double scalar)
{
return new Vector3D(a.X * scalar, a.Y * scalar, a.Z * scalar);
}
/// <summary>
/// 向量与标量除法运算符重载
/// </summary>
public static Vector3D operator /(Vector3D a, double scalar)
{
return new Vector3D(a.X / scalar, a.Y / scalar, a.Z / scalar);
}
/// <summary>
/// 向量反向
/// </summary>
public static Vector3D operator -(Vector3D a)
{
return new Vector3D(-a.X, -a.Y, -a.Z);
}
/// <summary>
/// 向量相等运算符重载
/// </summary>
public static bool operator ==(Vector3D left, Vector3D right)
{
return left.X == right.X && left.Y == right.Y && left.Z == right.Z;
}
/// <summary>
/// 向量不相等运算符重载
/// </summary>
public static bool operator !=(Vector3D left, Vector3D right)
{
return !(left == right);
}
/// <summary>
/// 判断两个向量是否相等
/// </summary>
public override readonly bool Equals(object? obj)
{
if (obj is Vector3D other)
{
return this == other;
}
return false;
}
/// <summary>
/// 获取向量的哈希码
/// </summary>
/// <returns>向量的哈希码</returns>
public override readonly int GetHashCode()
{
return HashCode.Combine(X, Y, Z);
}
/// <summary>
/// 计算向量的模长
/// </summary>
/// <returns>向量的模长</returns>
public readonly double Magnitude()
{
return Math.Sqrt(X * X + Y * Y + Z * Z);
}
/// <summary>
/// 计算向量模长的平方
/// </summary>
/// <returns>向量模长的平方</returns>
public readonly double MagnitudeSquared()
{
return X * X + Y * Y + Z * Z;
}
/// <summary>
/// 向量归一化
/// </summary>
/// <returns>归一化后的向量</returns>
/// <remarks>
/// 如果向量的模长小于 EPSILON则返回零向量
/// 否则返回单位向量
/// </remarks>
public readonly Vector3D Normalize()
{
double mag = Magnitude();
if (mag > EPSILON)
{
return new Vector3D(X / mag, Y / mag, Z / mag);
}
return Zero;
}
/// <summary>
/// 检查向量是否为单位向量
/// </summary>
/// <param name="tolerance">允许的误差范围,默认为 EPSILON</param>
/// <returns>如果向量是单位向量则返回 true否则返回 false</returns>
/// <remarks>
/// 通过检查向量的模长平方是否在 1.0 的 tolerance 范围内来判断
/// </remarks>
public readonly bool IsUnit(double tolerance = EPSILON)
{
return Math.Abs(MagnitudeSquared() - 1.0) < tolerance;
}
/// <summary>
/// 计算两个向量的叉积
/// </summary>
/// <param name="a">第一个向量</param>
/// <param name="b">第二个向量</param>
/// <returns>叉积结果</returns>
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
);
}
/// <summary>
/// 计算两个向量的点积
/// </summary>
/// <param name="a">第一个向量</param>
/// <param name="b">第二个向量</param>
/// <returns>点积结果</returns>
public static double DotProduct(Vector3D a, Vector3D b)
{
return a.X * b.X + a.Y * b.Y + a.Z * b.Z;
}
/// <summary>
/// 向量取反
/// </summary>
/// <param name="a">输入向量</param>
/// <returns>取反后的向量</returns>
public static Vector3D Negate(Vector3D a)
{
return new Vector3D(-a.X, -a.Y, -a.Z);
}
/// <summary>
/// 计算AB两点之间距离 A 点一定距离的点的坐标
/// </summary>
/// <param name="a">直线起点</param>
/// <param name="b">直线终点</param>
/// <param name="distance">距离起点距离</param>
/// <returns>直线上的点</returns>
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;
}
/// <summary>
/// 计算旋转矩阵
/// </summary>
/// <param name="orientation">方向</param>
/// <param name="misalignmentAngle">失调角</param>
/// <returns>旋转矩阵</returns>
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);
}
/// <summary>
/// 使用矩阵变换方向向量(不应用平移)
/// </summary>
/// <param name="direction">要变换的方向向量</param>
/// <param name="matrix">变换矩阵</param>
/// <returns>变换后的方向向量</returns>
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);
}
/// <summary>
/// 计算两个向量之间的角度
/// </summary>
/// <param name="a">第一个向量</param>
/// <param name="b">第二个向量</param>
/// <returns>两个向量之间的角度(弧度)</returns>
/// <remarks>
/// 该方法会先对向量进行归一化,然后计算它们之间的夹角。
/// 结果总是在 [0, π] 范围内。
/// </remarks>
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);
}
}
}

View File

@ -0,0 +1,103 @@
# RCS 模型设计 (`RcsPattern`)
`RcsPattern` 类用于管理目标实体在不同观察角度下的雷达散射截面 (Radar Cross Section, RCS) 数据。它采用一个详细的、基于观察者局部视角的模型来确定RCS值。
## 核心数据结构
- **内部RCS矩阵**: 核心数据存储在一个私有的 `double[,] RcsMatrixData` (6行4列) 中单位为dBsm (分贝平方米)。
- **数据源**: RCS数据通常从TOML配置文件中加载。为此类包含一个公共属性 `public List<List<double>> 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` 计算出的逻辑。对于"上"和"下"主方向,其"局部上/下"象限实际对应目标的全局"前/后"。

View File

@ -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

View File

@ -3,15 +3,10 @@ using ThreatSource.Simulation;
using ThreatSource.Utils; using ThreatSource.Utils;
using ThreatSource.Jammable; using ThreatSource.Jammable;
using ThreatSource.Jammer; using ThreatSource.Jammer;
using ThreatSource.Sensor;
using ThreatSource.Equipment; using ThreatSource.Equipment;
using ThreatSource.Guidance;
using ThreatSource.Indicator; using ThreatSource.Indicator;
using ThreatSource.Data; using ThreatSource.Data;
using AirTransmission;
using System.Diagnostics; using System.Diagnostics;
using System.Collections.Generic;
using System.Linq;
namespace ThreatSource.Tools.MissileSimulation namespace ThreatSource.Tools.MissileSimulation
{ {
@ -51,16 +46,16 @@ namespace ThreatSource.Tools.MissileSimulation
{ {
get get
{ {
switch (SelectedMissileId) return SelectedMissileId switch
{ {
case "LSGM_1": return "激光半主动制导导弹"; "LSGM_1" => "激光半主动制导导弹",
case "LBRM_1": return "激光驾束制导导弹"; "LBRM_1" => "激光驾束制导导弹",
case "TSM_1": return "末敏子弹药"; "TSM_1" => "末敏子弹药",
case "ICGM_1": return "红外指令制导导弹"; "ICGM_1" => "红外指令制导导弹",
case "ITGM_1": return "红外成像末制导导弹"; "ITGM_1" => "红外成像末制导导弹",
case "MMWG_1": return "毫米波末制导导弹"; "MMWG_1" => "毫米波末制导导弹",
default: return "未知导弹"; _ => "未知导弹",
} };
} }
} }
@ -69,12 +64,12 @@ namespace ThreatSource.Tools.MissileSimulation
/// </summary> /// </summary>
public ComprehensiveMissileSimulator() public ComprehensiveMissileSimulator()
{ {
// Initialize filter and listener first // 首先初始化过滤器和监听器
this.logLevelFilter = new EventTypeFilter(SourceLevels.Verbose); // Default to Verbose logLevelFilter = new EventTypeFilter(SourceLevels.Verbose); // 默认设置为Verbose
this.consoleListener = new ConsoleTraceListener(); consoleListener = new ConsoleTraceListener();
this.consoleListener.Filter = this.logLevelFilter; consoleListener.Filter = logLevelFilter;
// Clear any existing ConsoleTraceListeners and add our controlled instance // 清除任何现有的ConsoleTraceListeners并添加我们的控制实例
for (int i = Trace.Listeners.Count - 1; i >= 0; i--) for (int i = Trace.Listeners.Count - 1; i >= 0; i--)
{ {
if (Trace.Listeners[i] is ConsoleTraceListener) if (Trace.Listeners[i] is ConsoleTraceListener)
@ -82,7 +77,7 @@ namespace ThreatSource.Tools.MissileSimulation
Trace.Listeners.RemoveAt(i); Trace.Listeners.RemoveAt(i);
} }
} }
Trace.Listeners.Add(this.consoleListener); Trace.Listeners.Add(consoleListener);
simulationManager = new SimulationManager(); simulationManager = new SimulationManager();
_dataManager = new ThreatSourceDataManager("../ThreatSource/data"); _dataManager = new ThreatSourceDataManager("../ThreatSource/data");
@ -95,7 +90,7 @@ namespace ThreatSource.Tools.MissileSimulation
missileJammingMap = []; missileJammingMap = [];
SelectedMissileId = ""; SelectedMissileId = "";
this.activeJammings = new List<(JammingType Type, string DisplayName, string JammerId, string Mode, string Target)>(); activeJammings = [];
// 初始化导弹-干扰映射 // 初始化导弹-干扰映射
InitializeMissileJammingMap(); InitializeMissileJammingMap();
@ -231,7 +226,7 @@ namespace ThreatSource.Tools.MissileSimulation
Speed = 2.0 Speed = 2.0
}; };
string targetId = "Tank_1"; string targetId = "Tank_1";
var target = _threatSourceFactory.CreateTarget(targetId, "mbt_001", motionParameters); var target = _threatSourceFactory.CreateEquipment(targetId, "mbt_001", motionParameters);
targets[targetId] = target; targets[targetId] = target;
simulationManager.RegisterEntity(targetId, target); simulationManager.RegisterEntity(targetId, target);
Console.WriteLine($"添加目标 {targetId},位置:{motionParameters.Position}"); Console.WriteLine($"添加目标 {targetId},位置:{motionParameters.Position}");
@ -732,9 +727,9 @@ namespace ThreatSource.Tools.MissileSimulation
// Add to activeJammings list if not already present // Add to activeJammings list if not already present
var jammingTuple = (Type: type, DisplayName: displayName, JammerId: jammerId, Mode: mode, Target: target); 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 else
@ -979,7 +974,7 @@ namespace ThreatSource.Tools.MissileSimulation
Console.WriteLine($"关闭干扰器 {jammerId}: {displayName}"); Console.WriteLine($"关闭干扰器 {jammerId}: {displayName}");
// Remove from activeJammings list // 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(); PrintSimulationRegisteredComponents();
// 启动仿真系统 // 启动仿真系统
simulationManager.StartSimulation(this.TimeStep); simulationManager.StartSimulation(TimeStep);
while (simulationManager.State == SimulationState.Running) while (simulationManager.State == SimulationState.Running)
{ {
// 让仿真管理器统一处理所有实体的更新 // 让仿真管理器统一处理所有实体的更新
simulationManager.Update(this.TimeStep); simulationManager.Update(TimeStep);
// 检查是否还有活跃的导弹 // 检查是否还有活跃的导弹
var activeMissiles = simulationManager.GetEntitiesByType<BaseMissile>() var activeMissiles = simulationManager.GetEntitiesByType<BaseMissile>()
@ -1358,17 +1353,17 @@ namespace ThreatSource.Tools.MissileSimulation
public string GetActiveJammingDescription() public string GetActiveJammingDescription()
{ {
if (this.activeJammings == null || !this.activeJammings.Any()) if (activeJammings == null || activeJammings.Count == 0)
{ {
return "(无干扰)"; return "(无干扰)";
} }
return $"(已激活: {this.activeJammings.First().DisplayName})"; return $"(已激活: {activeJammings.First().DisplayName})";
} }
public List<(JammingType Type, string DisplayName, string JammerId, string Mode, string Target)> GetCurrentlyActiveJammings() 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];
} }
} }
} }

View File

@ -1,9 +1,4 @@
using ThreatSource.Utils;
using ThreatSource.Jammer;
using System;
using System.Threading;
using System.Diagnostics; using System.Diagnostics;
using System.Linq;
namespace ThreatSource.Tools.MissileSimulation namespace ThreatSource.Tools.MissileSimulation
{ {
@ -13,7 +8,7 @@ namespace ThreatSource.Tools.MissileSimulation
{ {
Console.WriteLine("综合导弹模拟程序启动..."); Console.WriteLine("综合导弹模拟程序启动...");
var simulator = new ComprehensiveMissileSimulator(); var simulator = new ComprehensiveMissileSimulator();
// Set a default log level on startup if desired, or rely on Simulator's default // 如果需要可以在启动时设置默认日志级别或依赖Simulator的默认值
// simulator.SetLogLevel(SourceLevels.Information); // simulator.SetLogLevel(SourceLevels.Information);
ApplicationHostLoop(simulator); ApplicationHostLoop(simulator);
Console.WriteLine("\n程序已退出。"); Console.WriteLine("\n程序已退出。");
@ -32,13 +27,13 @@ namespace ThreatSource.Tools.MissileSimulation
{ {
var indicators = simulator.GetActiveIndicators(); var indicators = simulator.GetActiveIndicators();
var firstActiveIndicator = indicators.FirstOrDefault(ind => ind.IsActive); 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})"; sensorStatusString = $"(已激活: {firstActiveIndicator.Name})";
} }
else else
{ {
sensorStatusString = "(无默认激活指示器)"; // Or "(配置)" or similar sensorStatusString = "(无默认激活指示器)";
} }
} }
else else
@ -98,8 +93,6 @@ namespace ThreatSource.Tools.MissileSimulation
} }
} }
} }
// Placeholder for ShowLogLevelMenu - to be implemented next
static void ShowLogLevelMenu(ComprehensiveMissileSimulator simulator) static void ShowLogLevelMenu(ComprehensiveMissileSimulator simulator)
{ {
bool backToMainMenu = false; bool backToMainMenu = false;
@ -115,8 +108,8 @@ namespace ThreatSource.Tools.MissileSimulation
Console.Write("请选择日志级别: "); Console.Write("请选择日志级别: ");
string input = Console.ReadLine()?.ToLower() ?? string.Empty; string input = Console.ReadLine()?.ToLower() ?? string.Empty;
SourceLevels currentLevel = simulator.CurrentLogLevel; // Use the new public property SourceLevels currentLevel = simulator.CurrentLogLevel;
SourceLevels selectedLevel = currentLevel; // Default to current if invalid choice SourceLevels selectedLevel = currentLevel; // 默认使用当前级别
bool levelSet = false; bool levelSet = false;
switch (input) switch (input)
@ -133,23 +126,22 @@ namespace ThreatSource.Tools.MissileSimulation
if (levelSet) if (levelSet)
{ {
simulator.SetLogLevel(selectedLevel); simulator.SetLogLevel(selectedLevel);
// Confirmation is printed by SetLogLevel in simulator // 设置日志级别后会由simulator打印确认
return; // Return to main menu after setting level return; // 设置后返回主菜单
} }
} }
} }
// Placeholder for RunSimulationInstance - to be implemented next
static void RunSimulationInstance(ComprehensiveMissileSimulator simulator) static void RunSimulationInstance(ComprehensiveMissileSimulator simulator)
{ {
Console.WriteLine("\n--- 开始仿真 --- "); Console.WriteLine("\n--- 开始仿真 --- ");
var simulationEndedEvent = new ManualResetEvent(false); var simulationEndedEvent = new ManualResetEvent(false);
simulator.SimulationEnded += (sender, e) => { simulator.SimulationEnded += (sender, e) => {
simulationEndedEvent.Set(); simulationEndedEvent.Set();
// Unsubscribe to prevent issues if RunSimulationInstance is called again // 防止多次调用RunSimulationInstance时出现的问题
if (sender is ComprehensiveMissileSimulator sim) // C# 7.0 pattern matching 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 键尝试提前中止当前仿真实例并返回主菜单。"); Console.WriteLine("仿真过程中,您可以随时按 Enter 键尝试提前中止当前仿真实例并返回主菜单。");
bool simulationForceStopped = false; bool simulationForceStopped = false;
while (!simulationEndedEvent.WaitOne(100)) // Check every 100ms if simulation ended naturally while (!simulationEndedEvent.WaitOne(100)) // 每100ms检查一次仿真是否结束
{ {
if (Console.KeyAvailable) if (Console.KeyAvailable)
{ {
// Read the key, but don't wait if it was just a check. var keyInfo = Console.ReadKey(intercept: true);
// 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
if (keyInfo.Key == ConsoleKey.Enter) if (keyInfo.Key == ConsoleKey.Enter)
{ {
Console.WriteLine("\n用户请求中止当前仿真实例..."); Console.WriteLine("\n用户请求中止当前仿真实例...");
simulator.Stop(); // Request simulator to stop simulator.Stop();
simulationForceStopped = true; 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(); simulationThread.Join();
if (simulationForceStopped) if (simulationForceStopped)
@ -194,7 +180,7 @@ namespace ThreatSource.Tools.MissileSimulation
} }
Console.WriteLine("按任意键返回主菜单..."); Console.WriteLine("按任意键返回主菜单...");
Console.ReadKey(); // Wait for user to acknowledge before returning to main menu Console.ReadKey(); // 等待用户确认后返回主菜单
} }
static void SelectMissile(ComprehensiveMissileSimulator simulator) static void SelectMissile(ComprehensiveMissileSimulator simulator)
@ -264,8 +250,7 @@ namespace ThreatSource.Tools.MissileSimulation
if (choice >= 1 && choice <= indicators.Length) if (choice >= 1 && choice <= indicators.Length)
{ {
simulator.ToggleIndicator(indicators[choice - 1].Id); simulator.ToggleIndicator(indicators[choice - 1].Id);
// Loop continues to show updated status return;
return; // Return to main menu after toggling sensor
} }
else else
{ {
@ -313,8 +298,6 @@ namespace ThreatSource.Tools.MissileSimulation
for (int i = 0; i < availableJammingOptions.Length; i++) for (int i = 0; i < availableJammingOptions.Length; i++)
{ {
var option = availableJammingOptions[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] ? "" : "")}] " + Console.WriteLine($" {i + 1}. {option.DisplayName} [{(jammingStatus[i] ? "" : "")}] " +
$"(干扰模式: {option.Mode}, 作用对象: {option.Target})"); $"(干扰模式: {option.Mode}, 作用对象: {option.Target})");
} }
@ -339,8 +322,7 @@ namespace ThreatSource.Tools.MissileSimulation
else else
simulator.ClearJamming(option.Type, option.DisplayName, option.JammerId); simulator.ClearJamming(option.Type, option.DisplayName, option.JammerId);
// Loop continues return;
return; // Return to main menu after applying/clearing jamming
} }
else else
{ {