无源侦察测向定位模型
模型设计
由于地球是一个椭球体,因此不能简单用平面的数学模型。考虑到我国地处东北半球,采用简化的 Mercator 投影,公式如下:
$$ \theta_i=\arccos \frac{\phi_e-\phi_i}{\sqrt{\left(\phi_e-\phi_i\right)^2+\left(\lambda_e-\lambda_i\right)^2 \cos ^2 \phi_i}} \tag{1} $$上式中辐射源目标的经纬度坐标为 $\left(\lambda_e, \phi_e\right)$, 测向真方位 $\theta_i$, 测向站坐标 $(\lambda_{\mathrm{i}}, \phi_{\mathrm{i}}$ )。
在实际应用中, 辐射源目标的坐标是一个估计值, 测向真方位含有误差, 测向站的坐标也可能含有误差。测向站坐标是可以修正的,在下面的讨论中假设是没有误差的。只讨论测向方位含有误差的情况。 $\lambda_e=$ 辐射源经度 $\phi_{\mathrm{e}}=$ 辐射源纬度 $\lambda_i=$ 测向站经度 $\phi_{\mathrm{i}}=$ 测向站纬度 $\theta_{\mathrm{i}}=$ 在第 i 个测向位置的辐射源真方位, 正北顺时针计算
如图1所示:
<img src=“https://cdn.mathpix.com/snip/images/JrF0XmCtGV-a9fLtExlAt3jkv1Xd9Sm51rE76nSnn_8.original.fullsize.png" /
根据(1)式, 我们将它写成如下形式:
$$ \cos \theta_i \sqrt{\left(\phi_e-\phi_i\right)^2+\left(\lambda_e-\lambda_i\right)^2 \cos ^2 \phi_i}=\phi_e-\phi_i $$将上式两边平方、移项、合并后再两边开方, 写成关于变量 $\lambda \mathrm{e}$ 和 $\phi_\mathrm{e}$ 的二元一次方程
$$ \cos \phi_i \cos \theta_{\mathrm{i}} \cdot \lambda_e-\sin \theta_{\mathrm{i}} \cdot \phi_e=\lambda_i \cos \phi_i \cos \theta_{\mathrm{i}}-\phi_i \sin \theta_{\mathrm{i}} $$对于多于一次的测向值, 写成矩阵形式如下:
$$ \mathbf{A}_n \cdot \mathbf{X}=\mathbf{W}_n $$其中
$$ \begin{array}{r} \mathbf{A}_n=\left[\begin{array}{cc} \cos \phi_1 \cos \theta_1 & -\sin \theta_1 \\ \vdots & \vdots \\ \cos \phi_n \cos \theta_n & -\sin \theta_n \end{array}\right], \quad \mathbf{W}_n=\left[\begin{array}{cc} \lambda_1 \cos \phi_1 \cos \theta_1 & -\phi_1 \sin \theta_1 \\ \vdots & \vdots \\ \lambda_n \cos \phi_n \cos \theta_n & -\phi_n \sin \theta_n \end{array}\right], \quad \mathbf{X}=\left[\begin{array}{l} \lambda_e \\ \phi_e \end{array}\right], \\ n \geq 2 \end{array} $$根据线性代数的理论, 有两次和两次以上的测向值就可以解出 $\lambda_e$ 和 $\phi_e$, 则 $\lambda_e$ 和 $\phi_e$ 的解向量 $\mathbf{X}$ 为
$$ \mathbf{X}=\mathbf{A}_n^{-1} \cdot \mathbf{W}_n $$模型适应性分析
整体来看,该模型在给定的前提和假设下是合理的。分析如下:
投影与模型设定:
模型中明确说明由地球的椭球体特性出发,而非简单的平面模型,为此采用了简化的 Mercator 投影。此类投影在局部范围内可以近似地将经纬度坐标转化为与平面坐标相类似的形式,从而使问题简化为类平面几何问题。在该投影下,将位置由$(\lambda,\phi)$映射到近似平面坐标时,一般会有对经度差异乘以$\cos \phi$的修正,以确保经纬度差在局部范围内与实际的东西向距离变化一致。这一点在模型中已体现:
$$\sqrt{(\phi_e-\phi_i)^2 + ((\lambda_e-\lambda_i)^2\cos^2\phi_i)}$$
中对经度差$(\lambda_e-\lambda_i)$引入了$\cos\phi_i$因子。方位角公式的合理性:
$$ \theta_i = \arctan2\big((\lambda_e-\lambda_i)\cos\phi_i, \phi_e-\phi_i\big) $$
一般在球面或近似平面上求点 $e(\lambda_e,\phi_e)$ 相对于点 $i(\lambda_i,\phi_i)$ 的方位角 $\theta_i$时,常用的简化公式为:给出的公式中对 $\cos \theta_i$ 的定义为:
$$ \cos \theta_i = \frac{\phi_e - \phi_i}{\sqrt{(\phi_e - \phi_i)^2 + ((\lambda_e - \lambda_i)^2 \cos^2\phi_i)}} $$如果从上述 $\arctan2$ 形式出发,可以发现:
- 分子方向为南北差 $(\phi_e - \phi_i)$,分母为总距离的平方根(类似直角三角形斜边长度)。
- $\arctan2$给出的是正北方向顺时针转动的角度,将 $\cos\theta_i$ 表达为上述比值是完全可行的,只需注意 $\arctan2$ 会自动考虑象限问题,而使用 $\arccos$ 需要小心相位。但在定义清晰的前提下,这是可接受的近似和定义方式。
线性化处理:
$$ \mathbf{A}_n \cdot \mathbf{X}=\mathbf{W}_n $$
文中将非线性关系通过平方、移项、合并后,最终导出了一组关于 $\lambda_e$、$\phi_e$ 的线性方程。给出了矩阵形式:其中 $\mathbf{X} = [\lambda_e, \phi_e]^T$,$\mathbf{A}_n$ 和 $\mathbf{W}_n$ 分别由测向站坐标与测量到的真方位角相关参数构成。在有两个及以上的测向方位数据时($n \geq 2$),就可以求解 $\mathbf{X}$,即辐射源目标的经纬度。这是典型的从多条“方位直线”求交点的过程(在局部近似的投影平面中成立)。
误差与假设:
模型中仅考虑测向方位的误差,假设测向站坐标无误差,这使得问题进一步简化。实际情况中,坐标与测量方位均可能存在误差,需要相应的误差传播与最小二乘优化来求解。但文中仅讨论理论模型的正确性与可解性,这一点不影响模型的基础正确性。
总结:
给定“采用简化的 Mercator 投影”、“已知测向站坐标无误差”、“测向真方位有已知(或可估计)误差”的前提条件下,文中给出的数学推导逻辑清晰、步骤合理,最终建立的二元一次线性方程组形式也是正确可行的。这套模型在一定近似下(局域范围内的地理坐标换算为近似平面问题)是成立的。
因此,从理论与推导过程来看,该模型是正确且合理的。
C++示例代码:
下面给出一个示例 C++ 代码,用于根据给定的测向站数据与测向真方位来求解辐射源目标的经纬度坐标 $\lambda_e, \phi_e$ 。该代码示例假设已经获得了多个测向数据点(测向站坐标 $\lambda_i, \phi_i$ 及测向方位 $\theta_i$),并使用线性代数方法求解。
这里采用 Eigen 作为线性代数库来求解线性方程组。
如果只有两组测向数据($n=2$),则对应的矩阵 $\mathbf{A}_n$ 为 $2\times2$,可直接求逆。
如果有多于两组数据($n>2$),则可采用最小二乘解(Normal Equation或Eigen内置的最小二乘求解)来获得 $\lambda_e, \phi_e$ 的估计值。
注意事项:
- 测向站坐标和角度需以弧度制输入和处理。
- 对于 $\cos\phi_i$ 和 $\sin\theta_i$ 等需要确保 $\phi_i, \theta_i$ 已经是弧度。
- 数据格式与获取方式需根据实际情况调整。
#include <iostream>
#include <vector>
#include <Eigen/Dense>
#include <cmath>
// 假设数据结构如下
struct Measurement {
double lambda_i; // 测向站经度(弧度)
double phi_i; // 测向站纬度(弧度)
double theta_i; // 测向方位角(弧度),正北为0,顺时针方向
};
int main() {
// 示例输入数据(需根据实际情况填写)
// 假设有 n 次测量,每次给定测向站位置 (lambda_i, phi_i) 和方位 theta_i
std::vector<Measurement> measurements = {
// 示例数据,请根据实际情况修改
{ 1.234, 0.456, 0.523 },
{ 1.235, 0.457, 1.047 },
{ 1.237, 0.460, 1.570 }
};
int n = static_cast<int>(measurements.size());
if (n < 2) {
std::cerr << "至少需要两组测向数据才能求解!" << std::endl;
return -1;
}
// 构建线性方程组 A * X = C
// 对应关系:
// A的每行: [cos(phi_i)*cos(theta_i), -sin(theta_i)]
// C的每行: lambda_i * cos(phi_i)*cos(theta_i) - phi_i*sin(theta_i)
//
// X = [lambda_e, phi_e]^T 是未知量。
Eigen::MatrixXd A(n, 2);
Eigen::VectorXd C(n);
for (int i = 0; i < n; ++i) {
double lambda_i = measurements[i].lambda_i;
double phi_i = measurements[i].phi_i;
double theta_i = measurements[i].theta_i;
double a_i = std::cos(phi_i)*std::cos(theta_i);
double b_i = -std::sin(theta_i);
double c_i = lambda_i * std::cos(phi_i)*std::cos(theta_i) - phi_i * std::sin(theta_i);
A(i, 0) = a_i;
A(i, 1) = b_i;
C(i) = c_i;
}
// 当 n=2 时可以直接 A.inverse()*C 求解
// 当 n>2 时,需要使用最小二乘求解,即 (A^T A)X = A^T C
// 使用Eigen提供的最小二乘求解器
Eigen::VectorXd X;
if (n == 2) {
// 2x2可直接求逆
Eigen::Matrix2d A2;
Eigen::Vector2d C2;
A2 << A(0,0), A(0,1),
A(1,0), A(1,1);
C2 << C(0), C(1);
X = A2.inverse() * C2;
} else {
// n > 2,用最小二乘法
// 正规方程: X = (A^T A)^{-1} A^T C
// 或直接使用Eigen的colPivHouseholderQr等分解进行求解
X = (A.transpose() * A).ldlt().solve(A.transpose() * C);
}
double lambda_e = X(0); // 求解出的辐射源经度
double phi_e = X(1); // 求解出的辐射源纬度
// 输出结果
std::cout << "求解得到的辐射源坐标:" << std::endl;
std::cout << "lambda_e = " << lambda_e << " (弧度)" << std::endl;
std::cout << "phi_e = " << phi_e << " (弧度)" << std::endl;
// 若需要,可将弧度转换为度
double lambda_e_deg = lambda_e * 180.0 / M_PI;
double phi_e_deg = phi_e * 180.0 / M_PI;
std::cout << "lambda_e (度) = " << lambda_e_deg << std::endl;
std::cout << "phi_e (度) = " << phi_e_deg << std::endl;
return 0;
}说明:
- 在上述代码中,我们假设输入的 $\lambda_i, \phi_i, \theta_i$ 已经是弧度制。如果输入为度,需要先转换为弧度。
- 当 $n=2$ 时,是标准的 2x2 方程组,可直接通过逆矩阵求解。
- 当 $n>2$ 时,为过定方程组,采用最小二乘方法(正规方程或QR分解等方法)来求解。上例使用
(A.transpose() * A).ldlt().solve(A.transpose() * C)来求最小二乘解。 - 实际中还应考虑误差和数值稳定性,可以使用更稳定的分解方法,如
A.colPivHouseholderQr().solve(C)来得到最小二乘解。 - 根据需求可以对输入和输出进行更严格的检查和格式化。
更稳定分解的版本
下面给出采用更稳定的 colPivHouseholderQr() 分解进行求解的示例代码。同样使用 Eigen 库来处理线性代数运算。在此示例中,只需稍作修改,通过 A.colPivHouseholderQr().solve(C) 来得到最小二乘解。
注意事项:
- 输入输出与前例相同。
- 同样假设输入的坐标和角度已转换为弧度。
- 当有两组数据(n=2)时仍可直接求逆解,但建议统一使用QR分解求解,因其对数值稳定性更好。
#include <iostream>
#include <vector>
#include <Eigen/Dense>
#include <cmath>
// 测量数据结构
struct Measurement {
double lambda_i; // 测向站经度 (弧度)
double phi_i; // 测向站纬度 (弧度)
double theta_i; // 测向方位角 (弧度)
};
int main() {
// 示例测量数据,需根据实际情况修改
std::vector<Measurement> measurements = {
{ 1.234, 0.456, 0.523 },
{ 1.235, 0.457, 1.047 },
{ 1.237, 0.460, 1.570 }
};
int n = static_cast<int>(measurements.size());
if (n < 2) {
std::cerr << "至少需要两组测向数据才能求解!" << std::endl;
return -1;
}
Eigen::MatrixXd A(n, 2);
Eigen::VectorXd C(n);
// 构建方程组 A * X = C
// A的每行对应: [cos(phi_i)*cos(theta_i), -sin(theta_i)]
// C的每行对应: lambda_i*cos(phi_i)*cos(theta_i) - phi_i*sin(theta_i)
for (int i = 0; i < n; ++i) {
double lambda_i = measurements[i].lambda_i;
double phi_i = measurements[i].phi_i;
double theta_i = measurements[i].theta_i;
double a_i = std::cos(phi_i)*std::cos(theta_i);
double b_i = -std::sin(theta_i);
double c_i = lambda_i * std::cos(phi_i)*std::cos(theta_i) - phi_i * std::sin(theta_i);
A(i, 0) = a_i;
A(i, 1) = b_i;
C(i) = c_i;
}
// 使用更加数值稳定的分解法求解最小二乘问题
Eigen::VectorXd X = A.colPivHouseholderQr().solve(C);
double lambda_e = X(0); // 辐射源经度 (弧度)
double phi_e = X(1); // 辐射源纬度 (弧度)
std::cout << "求解得到的辐射源坐标(弧度):" << std::endl;
std::cout << "lambda_e = " << lambda_e << std::endl;
std::cout << "phi_e = " << phi_e << std::endl;
// 若需要度制输出
double lambda_e_deg = lambda_e * 180.0 / M_PI;
double phi_e_deg = phi_e * 180.0 / M_PI;
std::cout << "辐射源坐标(度):" << std::endl;
std::cout << "lambda_e (deg) = " << lambda_e_deg << std::endl;
std::cout << "phi_e (deg) = " << phi_e_deg << std::endl;
return 0;
}通过上述修改,当测向数据点多于2个时,colPivHouseholderQr() 的方法能够在一定程度上提高数值稳定性和求解精度,对于实际中不适定或近似共线的情况能提供更稳定的结果。
无源侦察测向定位计算器
https://claude.site/artifacts/c9ff1d40-3bad-4001-97f7-f2c2030d30a6
- 其中辐射源位置用于与测向定位后结果做对比,使用了球面距离计算公式(Haversine公式)来计算两个经纬度点之间的距离。计算时使用地球平均半径为 6371000 米。
- 在结果显示部分增加了两个新的区域:
- “计算位置到各测向站的距离”:显示计算出的辐射源位置到每个测向站的距离
- “测向站之间的距离”:显示各个测向站之间的距离
import React, { useState } from 'react';
import { Card, CardHeader, CardTitle, CardContent } from '@/components/ui/card';
import { Button } from '@/components/ui/button';
import { Input } from '@/components/ui/input';
const RadiationCalculator = () => {
const [measurements, setMeasurements] = useState([
{ lambda_i: '', phi_i: '', theta_i: '' }
]);
const [actualSource, setActualSource] = useState({
lambda_e: '',
phi_e: ''
});
const [result, setResult] = useState(null);
// 计算两点间的距离(米)
const calculateDistance = (lat1, lon1, lat2, lon2) => {
const R = 6371000; // 地球平均半径(米)
const phi1 = lat1 * Math.PI / 180;
const phi2 = lat2 * Math.PI / 180;
const deltaPhi = (lat2 - lat1) * Math.PI / 180;
const deltaLambda = (lon2 - lon1) * Math.PI / 180;
const a = Math.sin(deltaPhi/2) * Math.sin(deltaPhi/2) +
Math.cos(phi1) * Math.cos(phi2) *
Math.sin(deltaLambda/2) * Math.sin(deltaLambda/2);
const c = 2 * Math.atan2(Math.sqrt(a), Math.sqrt(1-a));
const distance = R * c;
return distance;
};
// 计算所有站点之间的距离
const calculateAllDistances = (sourcePos, stations) => {
const distances = {
sourceToStations: [], // 辐射源到各测向站的距离
betweenStations: [] // 测向站之间的距离
};
// 计算辐射源到各测向站的距离
stations.forEach((station, idx) => {
const distance = calculateDistance(
sourcePos.phi,
sourcePos.lambda,
parseFloat(station.phi_i),
parseFloat(station.lambda_i)
);
distances.sourceToStations.push({
stationIndex: idx + 1,
distance: distance / 1000 // 转换为公里
});
});
// 计算测向站之间的距离
for (let i = 0; i < stations.length; i++) {
for (let j = i + 1; j < stations.length; j++) {
const distance = calculateDistance(
parseFloat(stations[i].phi_i),
parseFloat(stations[i].lambda_i),
parseFloat(stations[j].phi_i),
parseFloat(stations[j].lambda_i)
);
distances.betweenStations.push({
station1: i + 1,
station2: j + 1,
distance: distance / 1000 // 转换为公里
});
}
}
return distances;
};
const addMeasurement = () => {
setMeasurements([...measurements, { lambda_i: '', phi_i: '', theta_i: '' }]);
};
const removeMeasurement = (index) => {
if (measurements.length > 1) {
const newMeasurements = measurements.filter((_, i) => i !== index);
setMeasurements(newMeasurements);
}
};
const handleMeasurementChange = (index, field, value) => {
const newMeasurements = [...measurements];
newMeasurements[index][field] = value;
setMeasurements(newMeasurements);
};
const calculatePosition = () => {
// 将度数转换为弧度
const measurementsRad = measurements.map(m => ({
lambda_i: parseFloat(m.lambda_i) * Math.PI / 180,
phi_i: parseFloat(m.phi_i) * Math.PI / 180,
theta_i: parseFloat(m.theta_i) * Math.PI / 180
}));
const n = measurementsRad.length;
if (n < 2) {
alert('至少需要两组测向数据!');
return;
}
// 构建方程组
const A = new Array(n).fill(0).map(() => new Array(2).fill(0));
const C = new Array(n).fill(0);
for (let i = 0; i < n; i++) {
const { lambda_i, phi_i, theta_i } = measurementsRad[i];
const cos_phi = Math.cos(phi_i);
const cos_theta = Math.cos(theta_i);
const sin_theta = Math.sin(theta_i);
A[i][0] = cos_phi * cos_theta;
A[i][1] = -sin_theta;
C[i] = lambda_i * cos_phi * cos_theta - phi_i * sin_theta;
}
// 使用最小二乘法求解
const AT = A[0].map((_, colIndex) => A.map(row => row[colIndex]));
const ATA = AT.map(row => {
return A[0].map((_, j) => {
return row.reduce((sum, _, k) => sum + row[k] * A[k][j], 0);
});
});
const ATC = AT.map(row => {
return row.reduce((sum, _, i) => sum + row[i] * C[i], 0);
});
const det = ATA[0][0] * ATA[1][1] - ATA[0][1] * ATA[1][0];
const lambda_e = (ATA[1][1] * ATC[0] - ATA[0][1] * ATC[1]) / det;
const phi_e = (-ATA[1][0] * ATC[0] + ATA[0][0] * ATC[1]) / det;
// 转换回度数
const lambda_e_deg = lambda_e * 180 / Math.PI;
const phi_e_deg = phi_e * 180 / Math.PI;
// 计算与实际位置的偏差
const actual_lambda = parseFloat(actualSource.lambda_e);
const actual_phi = parseFloat(actualSource.phi_e);
const lambda_error = Math.abs(lambda_e_deg - actual_lambda);
const phi_error = Math.abs(phi_e_deg - actual_phi);
// 计算距离偏差
const distance_error = calculateDistance(
actual_phi,
actual_lambda,
phi_e_deg,
lambda_e_deg
);
// 计算所有站点之间的距离
const calculatedDistances = calculateAllDistances(
{ lambda: lambda_e_deg, phi: phi_e_deg },
measurements
);
const actualDistances = calculateAllDistances(
{ lambda: actual_lambda, phi: actual_phi },
measurements
);
setResult({
calculated: {
lambda: lambda_e_deg.toFixed(4),
phi: phi_e_deg.toFixed(4)
},
error: {
lambda: lambda_error.toFixed(4),
phi: phi_error.toFixed(4),
distance: (distance_error / 1000).toFixed(3) // 转换为公里
},
distances: {
calculated: calculatedDistances,
actual: actualDistances
}
});
};
return (
<Card className="w-full max-w-4xl">
<CardHeader>
<CardTitle>辐射源定位计算器</CardTitle>
</CardHeader>
<CardContent className="space-y-6">
<div className="space-y-4">
<div className="font-medium">实际辐射源位置(度):</div>
<div className="grid grid-cols-2 gap-4">
<div>
<label className="block text-sm mb-1">辐射源经度 (λe):</label>
<Input
type="number"
value={actualSource.lambda_e}
onChange={(e) => setActualSource({...actualSource, lambda_e: e.target.value})}
placeholder="输入实际经度"
/>
</div>
<div>
<label className="block text-sm mb-1">辐射源纬度 (φe):</label>
<Input
type="number"
value={actualSource.phi_e}
onChange={(e) => setActualSource({...actualSource, phi_e: e.target.value})}
placeholder="输入实际纬度"
/>
</div>
</div>
</div>
<div className="space-y-4">
<div className="font-medium">测向站数据(度):</div>
{measurements.map((m, index) => (
<div key={index} className="grid grid-cols-3 gap-4 items-end">
<div>
<label className="block text-sm mb-1">测向站经度 (λi):</label>
<Input
type="number"
value={m.lambda_i}
onChange={(e) => handleMeasurementChange(index, 'lambda_i', e.target.value)}
placeholder="输入测向站经度"
/>
</div>
<div>
<label className="block text-sm mb-1">测向站纬度 (φi):</label>
<Input
type="number"
value={m.phi_i}
onChange={(e) => handleMeasurementChange(index, 'phi_i', e.target.value)}
placeholder="输入测向站纬度"
/>
</div>
<div>
<label className="block text-sm mb-1">方位角 (θi):</label>
<div className="flex gap-2">
<Input
type="number"
value={m.theta_i}
onChange={(e) => handleMeasurementChange(index, 'theta_i', e.target.value)}
placeholder="输入方位角"
/>
{measurements.length > 1 && (
<Button
variant="destructive"
className="px-3"
onClick={() => removeMeasurement(index)}
>
删除
</Button>
)}
</div>
</div>
</div>
))}
</div>
<div className="space-x-4">
<Button onClick={addMeasurement}>添加测向站</Button>
<Button onClick={calculatePosition}>计算位置</Button>
</div>
{result && (
<div className="mt-6 p-4 bg-gray-100 rounded-lg">
<h3 className="font-medium mb-2">计算结果:</h3>
<div className="space-y-4">
<div>
<p className="font-medium">计算得到的位置:</p>
<p>经度 (λe) = {result.calculated.lambda}°</p>
<p>纬度 (φe) = {result.calculated.phi}°</p>
</div>
<div>
<p className="font-medium">与实际位置的偏差:</p>
<p>经度偏差 = {result.error.lambda}°</p>
<p>纬度偏差 = {result.error.phi}°</p>
<p>距离偏差 = {result.error.distance} 公里</p>
</div>
<div>
<p className="font-medium">计算位置到各测向站的距离:</p>
{result.distances.calculated.sourceToStations.map((dist, idx) => (
<p key={idx}>
到测向站{dist.stationIndex}: {dist.distance.toFixed(3)} 公里
</p>
))}
</div>
<div>
<p className="font-medium">测向站之间的距离:</p>
{result.distances.calculated.betweenStations.map((dist, idx) => (
<p key={idx}>
测向站{dist.station1}到测向站{dist.station2}: {dist.distance.toFixed(3)} 公里
</p>
))}
</div>
</div>
</div>
)}
</CardContent>
</Card>
);
};
export default RadiationCalculator;