Расчет с выравниванием СДИ

This commit is contained in:
emelianov
2026-06-15 16:05:45 +03:00
parent 88fb380be4
commit f49709d544
8 changed files with 115 additions and 10 deletions
@@ -201,6 +201,28 @@ abstract class AbstractPuudCalculator(
protected fun sdiForWd(wd: Vector3D, sickle: Boolean = false): Double = protected fun sdiForWd(wd: Vector3D, sickle: Boolean = false): Double =
(if (sickle) wd.z else wd.x) * config.focus (if (sickle) wd.z else wd.x) * config.focus
protected fun edgeLineOfSightVectorsInConnected(): Pair<Vector3D, Vector3D>? {
if (config.focus <= EPS || config.dlOep <= EPS) return null
val leftOffsetMm = config.dxOep - config.dlOep / 2.0
val rightOffsetMm = config.dxOep + config.dlOep / 2.0
return lineOfSightByFocalPlaneZ(leftOffsetMm) to lineOfSightByFocalPlaneZ(rightOffsetMm)
}
protected fun edgeSdiValues(
point: OrbitalPoint,
orientation: Orientation,
omegaConnected: Vector3D,
sickle: Boolean = false,
): Pair<Double, Double>? {
val (leftLine, rightLine) = edgeLineOfSightVectorsInConnected() ?: return null
val leftSdi = sdiForWd(wd(point, orientation, omegaConnected, leftLine), sickle)
val rightSdi = sdiForWd(wd(point, orientation, omegaConnected, rightLine), sickle)
return leftSdi to rightSdi
}
private fun lineOfSightByFocalPlaneZ(offsetMm: Double): Vector3D =
Vector3D(0.0, -config.focus, offsetMm).normSafe()
/** /**
* Аналог AbstractAISTPUUD::wd. Возвращает W/D в связанной СК для единственной центральной линии визирования. * Аналог AbstractAISTPUUD::wd. Возвращает W/D в связанной СК для единственной центральной линии визирования.
* *
@@ -209,16 +231,24 @@ abstract class AbstractPuudCalculator(
* добавляется радиальная составляющая изменения наклонной дальности, чтобы точка оставалась * добавляется радиальная составляющая изменения наклонной дальности, чтобы точка оставалась
* на поверхности эллипсоида. Такая схема соответствует формулам из OrbitalMotion::AbstractAISTPUUD::wd. * на поверхности эллипсоида. Такая схема соответствует формулам из OrbitalMotion::AbstractAISTPUUD::wd.
*/ */
protected fun wd(point: OrbitalPoint, orientation: Orientation, omegaConnected: Vector3D): Vector3D { protected fun wd(point: OrbitalPoint, orientation: Orientation, omegaConnected: Vector3D): Vector3D =
wd(point, orientation, omegaConnected, lineOfSightVectorInConnected())
protected fun wd(
point: OrbitalPoint,
orientation: Orientation,
omegaConnected: Vector3D,
lineConnected: Vector3D,
): Vector3D {
val ask = astro.grinvToASK(point) val ask = astro.grinvToASK(point)
val lineConnected = lineOfSightVectorInConnected() val lineInConnected = lineConnected.normSafe()
val connectedToOrbit = orientationMatrix(orientation) val connectedToOrbit = orientationMatrix(orientation)
val absToOrbBook = absToOrbBookMatrix(ask.r, ask.v) val absToOrbBook = absToOrbBookMatrix(ask.r, ask.v)
val orbitBookToAbs = absToOrbBook.transpose() val orbitBookToAbs = absToOrbBook.transpose()
val orbitToOrbitBook = orbBookToOrbMatrix().transpose() val orbitToOrbitBook = orbBookToOrbMatrix().transpose()
val absToGsk = Matrix3D().also { it.makeOzMatrix(-astro.si2000(point.t)) } val absToGsk = Matrix3D().also { it.makeOzMatrix(-astro.si2000(point.t)) }
val lineGsk = (absToGsk * orbitBookToAbs * orbitToOrbitBook * connectedToOrbit * lineConnected).normSafe() val lineGsk = (absToGsk * orbitBookToAbs * orbitToOrbitBook * connectedToOrbit * lineInConnected).normSafe()
if (lineGsk.module() < EPS) return Vector3D() if (lineGsk.module() < EPS) return Vector3D()
val earth = astro.earth val earth = astro.earth
@@ -310,6 +340,9 @@ abstract class AbstractPuudCalculator(
val eps = previous?.let { if (abs(t - it.t) > EPS) (omega - it.omega) / (t - it.t) else Vector3D() } ?: Vector3D() val eps = previous?.let { if (abs(t - it.t) > EPS) (omega - it.omega) / (t - it.t) else Vector3D() } ?: Vector3D()
val ground = pointOnEarth(orbital, orientation) val ground = pointOnEarth(orbital, orientation)
val wd = wd(orbital, orientation, omega) val wd = wd(orbital, orientation, omega)
val edgeSdi = edgeSdiValues(orbital, orientation, omega, sickle)
val centerSdi = sdiForWd(wd, sickle)
val sdiSpread = edgeSdi?.let { maxOf(abs(it.first - centerSdi), abs(it.second - centerSdi)) }
return AngularMotionPoint( return AngularMotionPoint(
t = t, t = t,
orbitalPoint = orbital, orbitalPoint = orbital,
@@ -319,7 +352,10 @@ abstract class AbstractPuudCalculator(
eps = eps, eps = eps,
quaternion = q, quaternion = q,
wd = wd, wd = wd,
sdi = sdiForWd(wd, sickle), sdi = centerSdi,
sdiLeft = edgeSdi?.first,
sdiRight = edgeSdi?.second,
sdiSpread = sdiSpread,
) )
} }
@@ -76,6 +76,12 @@ data class AngularMotionPoint(
val quaternion: Quaternion3D = Quaternion3D(1.0, 0.0, 0.0, 0.0), val quaternion: Quaternion3D = Quaternion3D(1.0, 0.0, 0.0, 0.0),
val wd: Vector3D = Vector3D(), val wd: Vector3D = Vector3D(),
val sdi: Double = 0.0, val sdi: Double = 0.0,
/** СДИ на левом краю линейки ОЭП, мм/с. */
val sdiLeft: Double? = null,
/** СДИ на правом краю линейки ОЭП, мм/с. */
val sdiRight: Double? = null,
/** Максимальное отклонение краевой СДИ от центральной, мм/с. */
val sdiSpread: Double? = null,
) )
/** Полный результат расчета режима углового движения. */ /** Полный результат расчета режима углового движения. */
@@ -152,6 +152,9 @@ open class AzimuthPUUD(
val ground = pointOnEarth(orbital, orientation) val ground = pointOnEarth(orbital, orientation)
val q = quaternionFor(orbital, orientation) val q = quaternionFor(orbital, orientation)
val wd = wd(orbital, orientation, omega) val wd = wd(orbital, orientation, omega)
val edgeSdi = edgeSdiValues(orbital, orientation, omega, sickle)
val centerSdi = sdiForWd(wd, sickle)
val sdiSpread = edgeSdi?.let { maxOf(abs(it.first - centerSdi), abs(it.second - centerSdi)) }
return AngularMotionPoint( return AngularMotionPoint(
t = t, t = t,
orbitalPoint = orbital, orbitalPoint = orbital,
@@ -161,11 +164,14 @@ open class AzimuthPUUD(
eps = eps, eps = eps,
quaternion = q, quaternion = q,
wd = wd, wd = wd,
sdi = sdiForWd(wd, sickle), sdi = centerSdi,
sdiLeft = edgeSdi?.first,
sdiRight = edgeSdi?.second,
sdiSpread = sdiSpread,
) )
} }
private fun visirToConnected(omegaVisir: Vector3D): Vector3D = protected fun visirToConnected(omegaVisir: Vector3D): Vector3D =
conToVisirMatrix().transpose() * omegaVisir conToVisirMatrix().transpose() * omegaVisir
protected fun initialVisirQuaternion( protected fun initialVisirQuaternion(
@@ -66,7 +66,47 @@ class SmoothSDIPUUD(
vsOptic.y - sdi / config.focus, vsOptic.y - sdi / config.focus,
) )
val omegaInVisir = visToOptic * programmed + (liv.inverse() * omegaEarth) val omegaInVisir = visToOptic * programmed + (liv.inverse() * omegaEarth)
return omegaInVisir return alignEdgeSdi(time, liv, omegaInVisir, sdi)
}
/**
* Вариант №3 требует не только Wz/D = 0 и заданную СДИ на центральной ЦЛВ,
* но и минимизацию изменения СДИ по ширине полосы. Центральная ЦЛВ не чувствительна
* к вращению вокруг себя, зато крайние лучи линейки ОЭП чувствительны. Поэтому
* подбираем добавку к компоненте угловой скорости вокруг ЦЛВ так, чтобы СДИ на
* левом и правом краях была максимально близка к заданной.
*/
private fun alignEdgeSdi(
time: Double,
liv: Quaternion3D,
omegaInVisir: Vector3D,
targetSdi: Double,
): Vector3D {
if (config.dlOep <= EPS || config.focus <= EPS) return omegaInVisir
val orbital = pointAt(time)
val orientation = orientationFromVisirQuaternion(orbital, liv)
fun edgeSdiFor(omegaVisir: Vector3D): Pair<Double, Double>? =
edgeSdiValues(orbital, orientation, visirToConnected(omegaVisir), sickle = false)
val baseEdges = edgeSdiFor(omegaInVisir) ?: return omegaInVisir
val probeOmega = Vector3D(omegaInVisir.x + LINE_SPIN_PROBE, omegaInVisir.y, omegaInVisir.z)
val probeEdges = edgeSdiFor(probeOmega) ?: return omegaInVisir
val leftK = (probeEdges.first - baseEdges.first) / LINE_SPIN_PROBE
val rightK = (probeEdges.second - baseEdges.second) / LINE_SPIN_PROBE
val denom = leftK * leftK + rightK * rightK
if (denom < EPS || !denom.isFinite()) return omegaInVisir
val correction = -((baseEdges.first - targetSdi) * leftK + (baseEdges.second - targetSdi) * rightK) / denom
if (!correction.isFinite()) return omegaInVisir
return Vector3D(
omegaInVisir.x + correction.coerceIn(-MAX_LINE_SPIN_CORRECTION, MAX_LINE_SPIN_CORRECTION),
omegaInVisir.y,
omegaInVisir.z,
)
} }
private fun horizontalGeodesicToAbs( private fun horizontalGeodesicToAbs(
@@ -93,4 +133,9 @@ class SmoothSDIPUUD(
gskToAbs * upGsk, gskToAbs * upGsk,
) )
} }
private companion object {
const val LINE_SPIN_PROBE = 1.0e-5
const val MAX_LINE_SPIN_CORRECTION = 0.05
}
} }
@@ -47,4 +47,7 @@ data class AngularMotionPointDTO(
val wdY: Double, val wdY: Double,
val wdZ: Double, val wdZ: Double,
val sdi: Double, val sdi: Double,
val sdiLeft: Double?,
val sdiRight: Double?,
val sdiSpread: Double?,
) )
@@ -117,6 +117,9 @@ class AngularMotionService(
wdY = point.wd.y, wdY = point.wd.y,
wdZ = point.wd.z, wdZ = point.wd.z,
sdi = point.sdi, sdi = point.sdi,
sdiLeft = point.sdiLeft,
sdiRight = point.sdiRight,
sdiSpread = point.sdiSpread,
) )
}, },
) )
@@ -451,7 +451,7 @@
el('angular-motion-export-csv').disabled = rows.length === 0; el('angular-motion-export-csv').disabled = rows.length === 0;
const body = el('angular-motion-result-body'); const body = el('angular-motion-result-body');
if (rows.length === 0) { if (rows.length === 0) {
body.innerHTML = '<tr><td colspan="14" class="text-center text-muted py-4">Нет данных</td></tr>'; body.innerHTML = '<tr><td colspan="17" class="text-center text-muted py-4">Нет данных</td></tr>';
return; return;
} }
body.innerHTML = rows.map(point => ` body.innerHTML = rows.map(point => `
@@ -470,6 +470,9 @@
<td>${escapeHtml(formatNumber(point.wdY, 7))}</td> <td>${escapeHtml(formatNumber(point.wdY, 7))}</td>
<td>${escapeHtml(formatNumber(point.wdZ, 7))}</td> <td>${escapeHtml(formatNumber(point.wdZ, 7))}</td>
<td>${escapeHtml(formatNumber(point.sdi, 4))}</td> <td>${escapeHtml(formatNumber(point.sdi, 4))}</td>
<td>${escapeHtml(formatNumber(point.sdiLeft, 4))}</td>
<td>${escapeHtml(formatNumber(point.sdiRight, 4))}</td>
<td>${escapeHtml(formatNumber(point.sdiSpread, 4))}</td>
</tr> </tr>
`).join(''); `).join('');
drawMap(true); drawMap(true);
@@ -500,7 +503,7 @@
function exportCsv() { function exportCsv() {
const rows = Array.isArray(state.result?.points) ? state.result.points : []; const rows = Array.isArray(state.result?.points) ? state.result.points : [];
if (rows.length === 0) return; if (rows.length === 0) return;
const header = ['time', 'revolution', 'tangDeg', 'krenDeg', 'riskDeg', 'groundLatitudeDeg', 'groundLongitudeDeg', 'omegaX', 'omegaY', 'omegaZ', 'wdX', 'wdY', 'wdZ', 'sdi']; const header = ['time', 'revolution', 'tangDeg', 'krenDeg', 'riskDeg', 'groundLatitudeDeg', 'groundLongitudeDeg', 'omegaX', 'omegaY', 'omegaZ', 'wdX', 'wdY', 'wdZ', 'sdi', 'sdiLeft', 'sdiRight', 'sdiSpread'];
const lines = [header.join(';')]; const lines = [header.join(';')];
rows.forEach(row => { rows.forEach(row => {
lines.push(header.map(key => row[key] ?? '').join(';')); lines.push(header.map(key => row[key] ?? '').join(';'));
@@ -158,10 +158,13 @@
<th>wd.y</th> <th>wd.y</th>
<th>wd.z</th> <th>wd.z</th>
<th>СДИ</th> <th>СДИ</th>
<th>СДИ лев.</th>
<th>СДИ прав.</th>
<th>ΔСДИ</th>
</tr> </tr>
</thead> </thead>
<tbody id="angular-motion-result-body"> <tbody id="angular-motion-result-body">
<tr><td colspan="14" class="text-center text-muted py-4">Нет данных</td></tr> <tr><td colspan="17" class="text-center text-muted py-4">Нет данных</td></tr>
</tbody> </tbody>
</table> </table>
</div> </div>